├── .github └── workflows │ ├── pythonpackage.yaml │ └── pythonpublish.yaml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── docs ├── Makefile ├── conf.py ├── doc-requirements.txt └── index.rst ├── isodist.png ├── makefile ├── pyproject.toml ├── setup.cfg ├── setup.py ├── src └── brainpy │ ├── __init__.py │ ├── _c │ ├── .gitignore │ ├── __init__.py │ ├── compat.h │ ├── compat.pxd │ ├── composition.pxd │ ├── composition.pyx │ ├── double_vector.pxd │ ├── double_vector.pyx │ ├── isotopic_constants.pxd │ ├── isotopic_constants.pyx │ ├── isotopic_distribution.pxd │ └── isotopic_distribution.pyx │ ├── _speedup.pxd │ ├── _speedup.pyx │ ├── brainpy.py │ ├── composition.py │ └── mass_dict.py └── tests ├── __init__.py ├── test_composition.py └── test_isotopic_distribution.py /.github/workflows/pythonpackage.yaml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: tests 5 | 6 | on: [push, pull_request] 7 | 8 | jobs: 9 | test: 10 | name: Test on ${{ matrix.os }} 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] 15 | os: [ubuntu-latest, windows-latest, macos-latest] 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Set up Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | - name: Install dependencies 23 | run: | 24 | pip install --upgrade pip setuptools wheel 25 | pip install coverage pytest pytest-cov -U 26 | pip install Cython 27 | pip install -v . 28 | - name: Test with pytest 29 | run: | 30 | make test 31 | -------------------------------------------------------------------------------- /.github/workflows/pythonpublish.yaml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: publish 5 | 6 | on: 7 | push: 8 | tags: 9 | - "v*" 10 | 11 | jobs: 12 | build-wheels: 13 | name: Build wheels on ${{ matrix.os }} 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | python-version: ['3.11'] 18 | os: [ubuntu-latest, windows-latest, macos-latest] 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Set up Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@v4 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | - name: Install dependencies 26 | run: | 27 | pip install --upgrade pip setuptools wheel 28 | pip install coverage pytest pytest-cov -U 29 | pip install Cython 30 | pip install . 31 | - name: Test with pytest 32 | run: | 33 | make test 34 | - name: Build source distributions 35 | run: | 36 | python setup.py build sdist 37 | - name: Install cibuildwheel 38 | run: python -m pip install cibuildwheel 39 | - name: Build wheels 40 | run: python -m cibuildwheel --output-dir dist/ 41 | env: 42 | CIBW_BUILD: "cp38* cp39* cp310* cp311* cp312*" 43 | CIBW_SKIP: "*_i686 *win32 *musllinux* pp*" 44 | CIBW_MANYLINUX_X86_64_IMAGE: "manylinux2014" 45 | CIBW_TEST_REQUIRES: "pytest" 46 | CIBW_BUILD_VERBOSITY: 5 47 | CIBW_ARCHS: "auto" 48 | - name: Upload Distributions 49 | uses: actions/upload-artifact@v4 50 | with: 51 | name: dist-${{ matrix.os }} 52 | path: | 53 | dist/*.whl 54 | dist/*.tar.gz 55 | retention-days: 7 56 | 57 | publish-wheels: 58 | needs: [build-wheels] 59 | runs-on: ubuntu-latest 60 | strategy: 61 | matrix: 62 | python-version: ['3.9'] 63 | steps: 64 | - uses: actions/checkout@v3 65 | - name: Set up Python ${{ matrix.python-version }} 66 | uses: actions/setup-python@v4 67 | with: 68 | python-version: ${{ matrix.python-version }} 69 | - uses: actions/download-artifact@v4.1.7 70 | with: 71 | path: dist/ 72 | pattern: dist-* 73 | merge-multiple: true 74 | - name: Install dependencies 75 | run: | 76 | python -m pip install -U pip setuptools wheel twine 77 | - name: "Publish" 78 | env: 79 | TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} 80 | TWINE_USERNAME: __token__ 81 | run: | 82 | ls -l dist/* 83 | twine upload dist/*.whl dist/*.tar.gz -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | .vscode 26 | src/brainpy/**.c 27 | src/brainpy/**.html 28 | .pytest* 29 | *sublime* 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *,cover 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | 57 | # Sphinx documentation 58 | docs/_build/ 59 | 60 | # PyBuilder 61 | target/ 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include src/brainpy *.pyx 2 | recursive-include src/brainpy *.pxd 3 | recursive-include src/brainpy *.c 4 | recursive-include src/brainpy *.h 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # brainpy 2 | A Python implementation of **B**affling **R**ecursive **A**lgorithm for **I**sotopic distributio**N** calculations (`BRAIN`). 3 | This is a direct translation of Han Hu's root-finding-free approach. 4 | 5 | Documentation: http://mobiusklein.github.io/brainpy 6 | 7 | Theoretical isotopic patterns appear when you can resolve distinct *isotopes* of an ion in a 8 | mass spectrum. Being able to predict the isotopic pattern of a molecule is useful for interpreting 9 | mass spectra to avoid counting the same ion with extra neutrons twice, recognizing the monoisotopic 10 | peak of a large multiply charged ion, or for discriminating among different elemental compositions 11 | of similar masses. 12 | 13 | `BRAIN` takes an elemental composition represented by any `Mapping`-like Python object 14 | and uses it to compute its aggregated isotopic distribution. All isotopic variants of the same 15 | number of neutrons are collapsed into a single centroid peak, meaning it does not consider 16 | isotopic fine structure. 17 | 18 | ```python 19 | from brainpy import isotopic_variants 20 | 21 | # Generate theoretical isotopic pattern 22 | peptide = {'H': 53, 'C': 34, 'O': 15, 'N': 7} 23 | theoretical_isotopic_cluster = isotopic_variants(peptide, npeaks=5, charge=1) 24 | for peak in theoretical_isotopic_cluster: 25 | print(peak.mz, peak.intensity) 26 | 27 | 28 | # All following code is to illustrate what brainpy just did. 29 | 30 | # produce a theoretical profile using a gaussian peak shape 31 | import numpy as np 32 | mz_grid = np.arange(theoretical_isotopic_cluster[0].mz - 1, 33 | theoretical_isotopic_cluster[-1].mz + 1, 0.02) 34 | intensity = np.zeros_like(mz_grid) 35 | sigma = 0.002 36 | for peak in theoretical_isotopic_cluster: 37 | # Add gaussian peak shape centered around each theoretical peak 38 | intensity += peak.intensity * np.exp(-(mz_grid - peak.mz) ** 2 / (2 * sigma) 39 | ) / (np.sqrt(2 * np.pi) * sigma) 40 | 41 | # Normalize profile to 0-100 42 | intensity = (intensity / intensity.max()) * 100 43 | 44 | # draw the profile 45 | from matplotlib import pyplot as plt 46 | plt.plot(mz_grid, intensity) 47 | plt.xlabel("m/z") 48 | plt.ylabel("Relative intensity") 49 | ``` 50 | 51 | ## Installing 52 | `brainpy` has three implementations, a pure Python implementation, a Cython translation 53 | of that implementation, and a pure C implementation that releases the `GIL`. 54 | 55 | To install from a package index, you will need to have a C compiler appropriate to your Python 56 | version to build these extension modules. Additionally, there are prebuilt wheels for Windows 57 | available on [PyPI](https://pypi.org/project/brain-isotopic-distribution/). 58 | 59 | `$ pip install brain-isotopic-distribution` 60 | 61 | To build from source, in addition to a C compiler you will also need to install a recent version 62 | of [Cython](https://pypi.org/project/Cython/) to transpile C code. 63 | 64 | 65 | ![An isotopic pattern](https://raw.githubusercontent.com/mobiusklein/brainpy/master/isodist.png) 66 | 67 | 68 | #### Original Algorithm: 69 | P. Dittwald, J. Claesen, T. Burzykowski, D. Valkenborg, and A. Gambin, “BRAIN: a universal tool for high-throughput calculations of the isotopic distribution for mass spectrometry.,” Anal. Chem., vol. 85, no. 4, pp. 1991–4, Feb. 2013. 70 | 71 | #### Original Implementation: 72 | H. Hu, P. Dittwald, J. Zaia, and D. Valkenborg, “Comment on ‘Computation of isotopic peak center-mass distribution by fourier transform’.,” Anal. Chem., vol. 85, no. 24, pp. 12189–92, Dec. 2013. 73 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | html: 55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | dirhtml: 60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 63 | 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | pickle: 70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 71 | @echo 72 | @echo "Build finished; now you can process the pickle files." 73 | 74 | json: 75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 76 | @echo 77 | @echo "Build finished; now you can process the JSON files." 78 | 79 | htmlhelp: 80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 81 | @echo 82 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 83 | ".hhp project file in $(BUILDDIR)/htmlhelp." 84 | 85 | qthelp: 86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 87 | @echo 88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/brainpy.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/brainpy.qhc" 93 | 94 | applehelp: 95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 96 | @echo 97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 98 | @echo "N.B. You won't be able to view it unless you put it in" \ 99 | "~/Library/Documentation/Help or install it in your application" \ 100 | "bundle." 101 | 102 | devhelp: 103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 104 | @echo 105 | @echo "Build finished." 106 | @echo "To view the help file:" 107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/brainpy" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/brainpy" 109 | @echo "# devhelp" 110 | 111 | epub: 112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 113 | @echo 114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 115 | 116 | latex: 117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 118 | @echo 119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 121 | "(use \`make latexpdf' here to do that automatically)." 122 | 123 | latexpdf: 124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 125 | @echo "Running LaTeX files through pdflatex..." 126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 128 | 129 | latexpdfja: 130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 131 | @echo "Running LaTeX files through platex and dvipdfmx..." 132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 134 | 135 | text: 136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 137 | @echo 138 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 139 | 140 | man: 141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 142 | @echo 143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 144 | 145 | texinfo: 146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 147 | @echo 148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 149 | @echo "Run \`make' in that directory to run these through makeinfo" \ 150 | "(use \`make info' here to do that automatically)." 151 | 152 | info: 153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 154 | @echo "Running Texinfo files through makeinfo..." 155 | make -C $(BUILDDIR)/texinfo info 156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 157 | 158 | gettext: 159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 160 | @echo 161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 162 | 163 | changes: 164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 165 | @echo 166 | @echo "The overview file is in $(BUILDDIR)/changes." 167 | 168 | linkcheck: 169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 170 | @echo 171 | @echo "Link check complete; look for any errors in the above output " \ 172 | "or in $(BUILDDIR)/linkcheck/output.txt." 173 | 174 | doctest: 175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 176 | @echo "Testing of doctests in the sources finished, look at the " \ 177 | "results in $(BUILDDIR)/doctest/output.txt." 178 | 179 | coverage: 180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 181 | @echo "Testing of coverage in the sources finished, look at the " \ 182 | "results in $(BUILDDIR)/coverage/python.txt." 183 | 184 | xml: 185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 186 | @echo 187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 188 | 189 | pseudoxml: 190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 191 | @echo 192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 193 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # brainpy documentation build configuration file, created by 4 | # sphinx-quickstart on Thu Oct 15 01:53:09 2015. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | import shlex 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | sys.path.insert(0, os.path.abspath('..')) 23 | 24 | # -- General configuration ------------------------------------------------ 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | #needs_sphinx = '1.0' 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be 30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 31 | # ones. 32 | extensions = [ 33 | 'sphinx.ext.autodoc', 34 | 'sphinx.ext.todo', 35 | 'sphinx.ext.viewcode', 36 | 'sphinx.ext.napoleon', 37 | 'matplotlib.sphinxext.plot_directive', 38 | 'sphinx.ext.intersphinx', 39 | 40 | ] 41 | 42 | intersphinx_mapping = {'python': ('https://docs.python.org/3', None)} 43 | 44 | # Add any paths that contain templates here, relative to this directory. 45 | templates_path = ['_templates'] 46 | 47 | # The suffix(es) of source filenames. 48 | # You can specify multiple suffix as a list of string: 49 | # source_suffix = ['.rst', '.md'] 50 | source_suffix = '.rst' 51 | 52 | # The encoding of source files. 53 | #source_encoding = 'utf-8-sig' 54 | 55 | # The master toctree document. 56 | master_doc = 'index' 57 | 58 | # General information about the project. 59 | project = u'brainpy' 60 | copyright = u'2015, Joshua Klein and Han Hu' 61 | author = u'Joshua Klein and Han Hu' 62 | 63 | # The version info for the project you're documenting, acts as replacement for 64 | # |version| and |release|, also used in various other places throughout the 65 | # built documents. 66 | # 67 | # The short X.Y version. 68 | version = '' 69 | # The full version, including alpha/beta/rc tags. 70 | release = '' 71 | 72 | # The language for content autogenerated by Sphinx. Refer to documentation 73 | # for a list of supported languages. 74 | # 75 | # This is also used if you do content translation via gettext catalogs. 76 | # Usually you set "language" from the command line for these cases. 77 | language = 'en' 78 | 79 | # There are two options for replacing |today|: either, you set today to some 80 | # non-false value, then it is used: 81 | #today = '' 82 | # Else, today_fmt is used as the format for a strftime call. 83 | #today_fmt = '%B %d, %Y' 84 | 85 | # List of patterns, relative to source directory, that match files and 86 | # directories to ignore when looking for source files. 87 | exclude_patterns = ['_build'] 88 | 89 | # The reST default role (used for this markup: `text`) to use for all 90 | # documents. 91 | #default_role = None 92 | 93 | # If true, '()' will be appended to :func: etc. cross-reference text. 94 | #add_function_parentheses = True 95 | 96 | # If true, the current module name will be prepended to all description 97 | # unit titles (such as .. function::). 98 | #add_module_names = True 99 | 100 | # If true, sectionauthor and moduleauthor directives will be shown in the 101 | # output. They are ignored by default. 102 | #show_authors = False 103 | 104 | # The name of the Pygments (syntax highlighting) style to use. 105 | pygments_style = 'sphinx' 106 | 107 | # A list of ignored prefixes for module index sorting. 108 | #modindex_common_prefix = [] 109 | 110 | # If true, keep warnings as "system message" paragraphs in the built documents. 111 | #keep_warnings = False 112 | 113 | # If true, `todo` and `todoList` produce output, else they produce nothing. 114 | todo_include_todos = True 115 | 116 | 117 | # -- Options for HTML output ---------------------------------------------- 118 | 119 | # The theme to use for HTML and HTML Help pages. See the documentation for 120 | # a list of builtin themes. 121 | html_theme = 'alabaster' 122 | 123 | # Theme options are theme-specific and customize the look and feel of a theme 124 | # further. For a list of options available for each theme, see the 125 | # documentation. 126 | #html_theme_options = {} 127 | 128 | # Add any paths that contain custom themes here, relative to this directory. 129 | #html_theme_path = [] 130 | 131 | # The name for this set of Sphinx documents. If None, it defaults to 132 | # " v documentation". 133 | #html_title = None 134 | 135 | # A shorter title for the navigation bar. Default is the same as html_title. 136 | #html_short_title = None 137 | 138 | # The name of an image file (relative to this directory) to place at the top 139 | # of the sidebar. 140 | #html_logo = None 141 | 142 | # The name of an image file (within the static path) to use as favicon of the 143 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 144 | # pixels large. 145 | #html_favicon = None 146 | 147 | # Add any paths that contain custom static files (such as style sheets) here, 148 | # relative to this directory. They are copied after the builtin static files, 149 | # so a file named "default.css" will overwrite the builtin "default.css". 150 | html_static_path = ['_static'] 151 | 152 | # Add any extra paths that contain custom files (such as robots.txt or 153 | # .htaccess) here, relative to this directory. These files are copied 154 | # directly to the root of the documentation. 155 | #html_extra_path = [] 156 | 157 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 158 | # using the given strftime format. 159 | #html_last_updated_fmt = '%b %d, %Y' 160 | 161 | # If true, SmartyPants will be used to convert quotes and dashes to 162 | # typographically correct entities. 163 | #html_use_smartypants = True 164 | 165 | # Custom sidebar templates, maps document names to template names. 166 | #html_sidebars = {} 167 | 168 | # Additional templates that should be rendered to pages, maps page names to 169 | # template names. 170 | #html_additional_pages = {} 171 | 172 | # If false, no module index is generated. 173 | #html_domain_indices = True 174 | 175 | # If false, no index is generated. 176 | #html_use_index = True 177 | 178 | # If true, the index is split into individual pages for each letter. 179 | #html_split_index = False 180 | 181 | # If true, links to the reST sources are added to the pages. 182 | #html_show_sourcelink = True 183 | 184 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 185 | #html_show_sphinx = True 186 | 187 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 188 | #html_show_copyright = True 189 | 190 | # If true, an OpenSearch description file will be output, and all pages will 191 | # contain a tag referring to it. The value of this option must be the 192 | # base URL from which the finished HTML is served. 193 | #html_use_opensearch = '' 194 | 195 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 196 | #html_file_suffix = None 197 | 198 | # Language to be used for generating the HTML full-text search index. 199 | # Sphinx supports the following languages: 200 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 201 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' 202 | #html_search_language = 'en' 203 | 204 | # A dictionary with options for the search language support, empty by default. 205 | # Now only 'ja' uses this config value 206 | #html_search_options = {'type': 'default'} 207 | 208 | # The name of a javascript file (relative to the configuration directory) that 209 | # implements a search results scorer. If empty, the default will be used. 210 | #html_search_scorer = 'scorer.js' 211 | 212 | # Output file base name for HTML help builder. 213 | htmlhelp_basename = 'brainpydoc' 214 | 215 | # -- Options for LaTeX output --------------------------------------------- 216 | 217 | latex_elements = { 218 | # The paper size ('letterpaper' or 'a4paper'). 219 | #'papersize': 'letterpaper', 220 | 221 | # The font size ('10pt', '11pt' or '12pt'). 222 | #'pointsize': '10pt', 223 | 224 | # Additional stuff for the LaTeX preamble. 225 | #'preamble': '', 226 | 227 | # Latex figure (float) alignment 228 | #'figure_align': 'htbp', 229 | } 230 | 231 | # Grouping the document tree into LaTeX files. List of tuples 232 | # (source start file, target name, title, 233 | # author, documentclass [howto, manual, or own class]). 234 | latex_documents = [ 235 | (master_doc, 'brainpy.tex', u'brainpy Documentation', 236 | u'Joshua Klein and Han Hu', 'manual'), 237 | ] 238 | 239 | # The name of an image file (relative to this directory) to place at the top of 240 | # the title page. 241 | #latex_logo = None 242 | 243 | # For "manual" documents, if this is true, then toplevel headings are parts, 244 | # not chapters. 245 | #latex_use_parts = False 246 | 247 | # If true, show page references after internal links. 248 | #latex_show_pagerefs = False 249 | 250 | # If true, show URL addresses after external links. 251 | #latex_show_urls = False 252 | 253 | # Documents to append as an appendix to all manuals. 254 | #latex_appendices = [] 255 | 256 | # If false, no module index is generated. 257 | #latex_domain_indices = True 258 | 259 | 260 | # -- Options for manual page output --------------------------------------- 261 | 262 | # One entry per manual page. List of tuples 263 | # (source start file, name, description, authors, manual section). 264 | man_pages = [ 265 | (master_doc, 'brainpy', u'brainpy Documentation', 266 | [author], 1) 267 | ] 268 | 269 | # If true, show URL addresses after external links. 270 | #man_show_urls = False 271 | 272 | 273 | # -- Options for Texinfo output ------------------------------------------- 274 | 275 | # Grouping the document tree into Texinfo files. List of tuples 276 | # (source start file, target name, title, author, 277 | # dir menu entry, description, category) 278 | texinfo_documents = [ 279 | (master_doc, 'brainpy', u'brainpy Documentation', 280 | author, 'brainpy', 'One line description of project.', 281 | 'Miscellaneous'), 282 | ] 283 | 284 | # Documents to append as an appendix to all manuals. 285 | #texinfo_appendices = [] 286 | 287 | # If false, no module index is generated. 288 | #texinfo_domain_indices = True 289 | 290 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 291 | #texinfo_show_urls = 'footnote' 292 | 293 | # If true, do not generate a @detailmenu in the "Top" node's menu. 294 | #texinfo_no_detailmenu = False 295 | 296 | 297 | # -- Options for Epub output ---------------------------------------------- 298 | 299 | # Bibliographic Dublin Core info. 300 | epub_title = project 301 | epub_author = author 302 | epub_publisher = author 303 | epub_copyright = copyright 304 | 305 | # The basename for the epub file. It defaults to the project name. 306 | #epub_basename = project 307 | 308 | # The HTML theme for the epub output. Since the default themes are not optimized 309 | # for small screen space, using the same theme for HTML and epub output is 310 | # usually not wise. This defaults to 'epub', a theme designed to save visual 311 | # space. 312 | #epub_theme = 'epub' 313 | 314 | # The language of the text. It defaults to the language option 315 | # or 'en' if the language is not set. 316 | #epub_language = '' 317 | 318 | # The scheme of the identifier. Typical schemes are ISBN or URL. 319 | #epub_scheme = '' 320 | 321 | # The unique identifier of the text. This can be a ISBN number 322 | # or the project homepage. 323 | #epub_identifier = '' 324 | 325 | # A unique identification for the text. 326 | #epub_uid = '' 327 | 328 | # A tuple containing the cover image and cover page html template filenames. 329 | #epub_cover = () 330 | 331 | # A sequence of (type, uri, title) tuples for the guide element of content.opf. 332 | #epub_guide = () 333 | 334 | # HTML files that should be inserted before the pages created by sphinx. 335 | # The format is a list of tuples containing the path and title. 336 | #epub_pre_files = [] 337 | 338 | # HTML files shat should be inserted after the pages created by sphinx. 339 | # The format is a list of tuples containing the path and title. 340 | #epub_post_files = [] 341 | 342 | # A list of files that should not be packed into the epub file. 343 | epub_exclude_files = ['search.html'] 344 | 345 | # The depth of the table of contents in toc.ncx. 346 | #epub_tocdepth = 3 347 | 348 | # Allow duplicate toc entries. 349 | #epub_tocdup = True 350 | 351 | # Choose between 'default' and 'includehidden'. 352 | #epub_tocscope = 'default' 353 | 354 | # Fix unsupported image types using the Pillow. 355 | #epub_fix_images = False 356 | 357 | # Scale large images. 358 | #epub_max_image_width = 0 359 | 360 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 361 | #epub_show_urls = 'inline' 362 | 363 | # If false, no index is generated. 364 | #epub_use_index = True 365 | -------------------------------------------------------------------------------- /docs/doc-requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx>=1.3 2 | matplotlib -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. brainpy documentation master file, created by 2 | sphinx-quickstart on Thu Oct 15 01:53:09 2015. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to brainpy's documentation! 7 | =================================== 8 | 9 | .. contents:: Table of Contents 10 | :depth: 3 11 | 12 | 13 | :mod:`brainpy` is a small Python library implementing the *B* afflingly *R* ecursive 14 | *A* lgorithm for *I* sotopic Patter *N* generation [Dittwald2014]_. It includes three implementations, 15 | a pure-Python object oriented implementation, a :title-reference:`Cython` accelerated 16 | version of the object oriented implementation, and a pure :title-reference:`C` implementation, 17 | listed in order of ascending speed. The C implementation is used by default when available. 18 | 19 | BRAIN, implemented in :func:`brainpy.isotopic_variants`, takes an elemental 20 | composition represented by any :class:`~.collections.abc.Mapping`-like Python object and uses 21 | it to compute its aggregated isotopic distribution. All isotopic variants of the same number 22 | of neutrons are collapsed into a single centroid peak, meaning it does not consider isotopic 23 | fine structure. 24 | 25 | .. plot:: 26 | :include-source: 27 | 28 | from brainpy import isotopic_variants 29 | 30 | # Generate theoretical isotopic pattern 31 | peptide = {'H': 53, 'C': 34, 'O': 15, 'N': 7} 32 | theoretical_isotopic_cluster = isotopic_variants(peptide, npeaks=5, charge=1) 33 | for peak in theoretical_isotopic_cluster: 34 | print(peak.mz, peak.intensity) 35 | 36 | # All following code is to illustrate what brainpy just did. 37 | 38 | # produce a theoretical profile using a gaussian peak shape 39 | import numpy as np 40 | mz_grid = np.arange(theoretical_isotopic_cluster[0].mz - 1, 41 | theoretical_isotopic_cluster[-1].mz + 1, 0.02) 42 | intensity = np.zeros_like(mz_grid) 43 | sigma = 0.002 44 | for peak in theoretical_isotopic_cluster: 45 | # Add gaussian peak shape centered around each theoretical peak 46 | intensity += peak.intensity * np.exp(-(mz_grid - peak.mz) ** 2 / (2 * sigma) 47 | ) / (np.sqrt(2 * np.pi) * sigma) 48 | 49 | # Normalize profile to 0-100 50 | intensity = (intensity / intensity.max()) * 100 51 | 52 | # draw the profile 53 | from matplotlib import pyplot as plt 54 | plt.plot(mz_grid, intensity) 55 | plt.xlabel("m/z") 56 | plt.ylabel("Relative intensity") 57 | 58 | 59 | Installing 60 | ---------- 61 | :mod:`brainpy` has three implementations, a pure Python implementation, a Cython translation 62 | of that implementation, and a pure C implementation that releases the :title-reference:`GIL`. 63 | 64 | To install from a package index, you will need to have a C compiler appropriate to your Python 65 | version to build these extension modules. Additionally, there are prebuilt wheels for Windows 66 | available on `PyPI `_: 67 | 68 | .. code-block:: sh 69 | 70 | $ pip install brain-isotopic-distribution 71 | 72 | To build from source, in addition to a C compiler you will also need to install a recent version 73 | of `Cython `_ to transpile C code. 74 | 75 | 76 | Usage 77 | ----- 78 | 79 | :mod:`brainpy` provides a single top-level function for taking a :class:`~collections.abc.Mapping`-like object 80 | defining a elemental composition and generating an isotopic pattern from it, :func:`brainpy.isotopic_variants`. 81 | 82 | 83 | Module Reference 84 | ---------------- 85 | 86 | .. automodule:: brainpy 87 | 88 | .. autofunction:: isotopic_variants 89 | 90 | .. autofunction:: max_variants 91 | 92 | .. autofunction:: calculate_mass 93 | 94 | .. autofunction:: parse_formula 95 | 96 | .. autoclass:: PyComposition 97 | :members: 98 | 99 | Supporting Objects 100 | ****************** 101 | 102 | .. autoclass:: Peak 103 | 104 | .. autoclass:: IsotopicDistribution 105 | 106 | .. automethod:: aggregated_isotopic_variants 107 | 108 | 109 | 110 | Indices and tables 111 | ================== 112 | 113 | * :ref:`genindex` 114 | * :ref:`modindex` 115 | * :ref:`search` 116 | 117 | 118 | Bibliography 119 | ============ 120 | 121 | .. [Dittwald2014] 122 | Dittwald, P., & Valkenborg, D. (2014). BRAIN 2.0: time and memory complexity improvements in the algorithm for calculating the isotope distribution. Journal of the American Society for Mass Spectrometry, 25(4), 588–94. https://doi.org/10.1007/s13361-013-0796-5 123 | -------------------------------------------------------------------------------- /isodist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mobiusklein/brainpy/f2ee5be540019ac833bbd1767a02fa182016d30a/isodist.png -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | dev: 2 | python setup.py develop 3 | 4 | test: 5 | py.test -v ./tests --cov=brainpy --cov-report=html --cov-report term 6 | 7 | retest: 8 | py.test -v ./tests --lf 9 | 10 | update-docs: 11 | git checkout gh-pages 12 | git pull origin master 13 | cd docs && make html 14 | git add docs/_build/html -f 15 | git commit -m "update docs" 16 | git push origin gh-pages 17 | git checkout master 18 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", 3 | "wheel", 4 | "Cython"] 5 | 6 | [tool.ruff] 7 | target-version = "py38" 8 | line-length = 120 -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [build_sphinx] 2 | source-dir = docs/ 3 | build-dir = docs/_build 4 | all_files = 1 5 | 6 | [upload_sphinx] 7 | upload-dir = docs/_build/html 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from setuptools import setup, find_packages, Extension 3 | from setuptools.command.build_ext import build_ext 4 | import sys 5 | import os 6 | import traceback 7 | import functools 8 | 9 | 10 | def has_option(name): 11 | try: 12 | sys.argv.remove('--%s' % name) 13 | return True 14 | except ValueError: 15 | pass 16 | # allow passing all cmd line options also as environment variables 17 | env_val = os.getenv(name.upper().replace('-', '_'), 'false').lower() 18 | if env_val == "true": 19 | return True 20 | return False 21 | 22 | 23 | include_diagnostics = has_option("include-diagnostics") 24 | force_cythonize = has_option("force-cythonize") 25 | 26 | 27 | try: 28 | from Cython.Build import cythonize 29 | cython_directives = { 30 | 'embedsignature': True, 31 | "profile": include_diagnostics 32 | } 33 | if include_diagnostics: 34 | Extension = functools.partial(Extension, define_macros=[ 35 | ("CYTHON_TRACE_NOGIL", "1"), 36 | ]) 37 | extensions = cythonize([ 38 | Extension( 39 | name="brainpy._speedup", sources=["src/brainpy/_speedup.pyx"], 40 | include_dirs=["src/brainpy/_c/"]), 41 | Extension( 42 | name="brainpy._c.composition", sources=["src/brainpy/_c/composition.pyx"], 43 | include_dirs=["src/brainpy/_c/"]), 44 | Extension( 45 | name="brainpy._c.double_vector", sources=["src/brainpy/_c/double_vector.pyx"], 46 | include_dirs=["src/brainpy/_c/"]), 47 | Extension( 48 | name="brainpy._c.isotopic_constants", sources=["src/brainpy/_c/isotopic_constants.pyx"], 49 | include_dirs=["src/brainpy/_c/"]), 50 | Extension( 51 | name="brainpy._c.isotopic_distribution", sources=["src/brainpy/_c/isotopic_distribution.pyx"], 52 | include_dirs=["src/brainpy/_c/"]) 53 | ], compiler_directives=cython_directives, force=force_cythonize) 54 | except ImportError: 55 | extensions = ([ 56 | Extension( 57 | name="brainpy._speedup", sources=["src/brainpy/_speedup.c"], 58 | include_dirs=["src/brainpy/_c/"]), 59 | Extension( 60 | name="brainpy._c.composition", sources=["src/brainpy/_c/composition.c"], 61 | include_dirs=["src/brainpy/_c/"]), 62 | Extension( 63 | name="brainpy._c.double_vector", sources=["src/brainpy/_c/double_vector.c"], 64 | include_dirs=["src/brainpy/_c/"]), 65 | Extension( 66 | name="brainpy._c.isotopic_constants", sources=["src/brainpy/_c/isotopic_constants.c"], 67 | include_dirs=["src/brainpy/_c/"]), 68 | Extension( 69 | name="brainpy._c.isotopic_distribution", sources=["src/brainpy/_c/isotopic_distribution.c"], 70 | include_dirs=["src/brainpy/_c/"]) 71 | ]) 72 | 73 | 74 | class BuildFailed(Exception): 75 | 76 | def __init__(self): 77 | self.cause = sys.exc_info()[1] # work around py 2/3 different syntax 78 | 79 | def __str__(self): 80 | return str(self.cause) 81 | 82 | 83 | class ve_build_ext(build_ext): 84 | # This class allows C extension building to fail. 85 | 86 | def run(self): 87 | try: 88 | build_ext.run(self) 89 | except Exception: 90 | traceback.print_exc() 91 | raise BuildFailed() 92 | 93 | def build_extension(self, ext): 94 | try: 95 | build_ext.build_extension(self, ext) 96 | except Exception: 97 | traceback.print_exc() 98 | raise BuildFailed() 99 | except ValueError: 100 | # this can happen on Windows 64 bit, see Python issue 7511 101 | traceback.print_exc() 102 | if "'path'" in str(sys.exc_info()[1]): # works with both py 2/3 103 | raise BuildFailed() 104 | raise 105 | 106 | 107 | cmdclass = {} 108 | 109 | cmdclass['build_ext'] = ve_build_ext 110 | 111 | 112 | def status_msgs(*msgs): 113 | print('*' * 75) 114 | for msg in msgs: 115 | print(msg) 116 | print('*' * 75) 117 | 118 | 119 | def run_setup(include_cext=True): 120 | setup( 121 | name='brain-isotopic-distribution', 122 | version='1.5.20', 123 | packages=find_packages(where='src'), 124 | package_dir={"": "src"}, 125 | description="Fast and efficient theoretical isotopic profile generation", 126 | long_description=""" 127 | :mod:`brainpy` is a small Python library implementing the *B* afflingly *R* ecursive 128 | *A* lgorithm for *I* sotopic Patter *N* generation [Dittwald2014]_. It includes three implementations, 129 | a pure-Python object oriented implementation, a :title-reference:`Cython` accelerated 130 | version of the object oriented implementation, and a pure :title-reference:`C` implementation, 131 | listed in order of ascending speed. The C implementation is used by default when available. 132 | 133 | BRAIN, implemented in :func:`brainpy.isotopic_variants`, takes an elemental 134 | composition represented by any :class:`~.collections.abc.Mapping`-like Python object and uses 135 | it to compute its aggregated isotopic distribution. All isotopic variants of the same number 136 | of neutrons are collapsed into a single centroid peak, meaning it does not consider isotopic 137 | fine structure. 138 | 139 | Installing 140 | ---------- 141 | :mod:`brainpy` has three implementations, a pure Python implementation, a Cython translation 142 | of that implementation, and a pure C implementation that releases the :title-reference:`GIL`. 143 | 144 | To install from a package index, you will need to have a C compiler appropriate to your Python 145 | version to build these extension modules. Additionally, there are prebuilt wheels for Windows 146 | available on `PyPI `_: 147 | 148 | .. code-block:: sh 149 | 150 | $ pip install brain-isotopic-distribution 151 | 152 | To build from source, in addition to a C compiler you will also need to install a recent version 153 | of `Cython `_ to transpile C code. 154 | 155 | References 156 | ---------- 157 | 158 | This package is an implementation of the algorithm originally described in 159 | P. Dittwald, J. Claesen, T. Burzykowski, D. Valkenborg, and A. Gambin, 160 | "BRAIN: a universal tool for high-throughput calculations of the isotopic distribution for mass spectrometry.", 161 | Anal. Chem., vol. 85, no. 4, pp. 1991–4, Feb. 2013. 162 | 163 | H. Hu, P. Dittwald, J. Zaia, and D. Valkenborg, 164 | "Comment on 'Computation of isotopic peak center-mass distribution by fourier transform'.", 165 | Anal. Chem., vol. 85, no. 24, pp. 12189–92, Dec. 2013. 166 | """, 167 | long_description_content_type='text/markdown', 168 | author=', '.join(["Joshua Klein", "Han Hu"]), 169 | author_email="jaklein@bu.edu", 170 | url="https://github.com/mobiusklein/brainpy", 171 | maintainer='Joshua Klein', 172 | keywords=["isotopic distribution", "isotopic pattern"], 173 | maintainer_email="jaklein@bu.edu", 174 | ext_modules=extensions if include_cext else None, 175 | include_package_data=True, 176 | cmdclass=cmdclass, 177 | classifiers=[ 178 | 'Development Status :: 4 - Beta', 179 | 'Intended Audience :: Science/Research', 180 | 'License :: OSI Approved :: Apache Software License', 181 | 'Topic :: Scientific/Engineering :: Bio-Informatics'], 182 | project_urls={ 183 | "Bug Tracker": "https://github.com/mobiusklein/brainpy/issues", 184 | "Source Code": "https://github.com/mobiusklein/brainpy", 185 | "Documentation": "http://mobiusklein.github.io/brainpy", 186 | } 187 | ) 188 | 189 | 190 | if __name__ == '__main__': 191 | try: 192 | run_setup(True) 193 | except Exception: 194 | run_setup(False) 195 | 196 | status_msgs( 197 | "WARNING: The C extension could not be compiled, " + 198 | "speedups are not enabled.", 199 | "Plain-Python build succeeded." 200 | ) 201 | -------------------------------------------------------------------------------- /src/brainpy/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | A Python Implementation of the 3 | Baffling Recursive Algorithm for Isotopic cluster distributioN 4 | """ 5 | import os 6 | 7 | from .brainpy import (isotopic_variants, IsotopicDistribution, periodic_table, 8 | max_variants, calculate_mass, neutral_mass, mass_charge_ratio, 9 | PROTON, _has_c, Peak, max_variants_approx) 10 | 11 | from .composition import parse_formula, PyComposition 12 | 13 | 14 | SimpleComposition = PyComposition 15 | 16 | 17 | def get_include(): 18 | """ 19 | Retrieve the path to compiled C extensions' source files to make linking simple. 20 | 21 | This module contains two variants of the algorithm reimplimented using C and the 22 | Python-C API. 23 | 24 | The `_speedup` module is a direct translation of the pure Python implementation 25 | using Cython, using static typing and `cdef class` versions of the existing classes. 26 | As this implementation still spends a substantial amount of time in Python-space, 27 | it is slower than the option below, but is more straight-forward to manipulate from 28 | Python. 29 | 30 | The `_c` module is a complete rewrite of the algorithm directly in C, using Python 31 | only to load mass configuration information and is fully usable. It exports an 32 | entrypoint function to Python which replaces the :func:`isotopic_variants` function 33 | when available. Because almost all of the action happens in C here, it's not 34 | possible to run individual parts of the process directly from Python. 35 | """ 36 | return os.path.join(__path__[0], "_c") 37 | 38 | 39 | if _has_c: 40 | from .brainpy import _IsotopicDistribution 41 | 42 | __author__ = "Joshua Klein & Han Hu" 43 | 44 | 45 | __all__ = [ 46 | "isotopic_variants", "IsotopicDistribution", "periodic_table", 47 | "max_variants", "calculate_mass", "neutral_mass", "mass_charge_ratio", 48 | "PROTON", "_has_c", "Peak", "max_variants_approx", 49 | 50 | "parse_formula", "PyComposition", "SimpleComposition", 51 | 52 | "_IsotopicDistribution", 53 | 54 | "get_include" 55 | ] 56 | -------------------------------------------------------------------------------- /src/brainpy/_c/.gitignore: -------------------------------------------------------------------------------- 1 | composition.c 2 | double_vector.c 3 | isotopic_constants.c 4 | isotopic_distribution.c 5 | *.html -------------------------------------------------------------------------------- /src/brainpy/_c/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mobiusklein/brainpy/f2ee5be540019ac833bbd1767a02fa182016d30a/src/brainpy/_c/__init__.py -------------------------------------------------------------------------------- /src/brainpy/_c/compat.h: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2015, Red Hat, Inc. and/or its affiliates 2 | * Licensed under the MIT license; see py3c.h 3 | */ 4 | 5 | #ifndef _PY3C_COMPAT_H_ 6 | #define _PY3C_COMPAT_H_ 7 | #include 8 | 9 | #if PY_MAJOR_VERSION >= 3 10 | 11 | /***** Python 3 *****/ 12 | 13 | #define IS_PY3 1 14 | 15 | /* Strings */ 16 | 17 | #define PyStr_Type PyUnicode_Type 18 | #define PyStr_Check PyUnicode_Check 19 | #define PyStr_CheckExact PyUnicode_CheckExact 20 | #define PyStr_FromString PyUnicode_FromString 21 | #define PyStr_FromStringAndSize PyUnicode_FromStringAndSize 22 | #define PyStr_FromFormat PyUnicode_FromFormat 23 | #define PyStr_FromFormatV PyUnicode_FromFormatV 24 | #define PyStr_AsString PyUnicode_AsUTF8 25 | 26 | #define PyStr_Format PyUnicode_Format 27 | #define PyStr_InternInPlace PyUnicode_InternInPlace 28 | #define PyStr_InternFromString PyUnicode_InternFromString 29 | #define PyStr_Decode PyUnicode_Decode 30 | 31 | #define PyStr_AsUTF8String PyUnicode_AsUTF8String // returns PyBytes 32 | #define PyStr_AsUTF8 PyUnicode_AsUTF8 33 | #define PyStr_AsUTF8AndSize PyUnicode_AsUTF8AndSize 34 | 35 | /* Ints */ 36 | 37 | #define PyInt_Type PyLong_Type 38 | #define PyInt_Check PyLong_Check 39 | #define PyInt_CheckExact PyLong_CheckExact 40 | #define PyInt_FromString PyLong_FromString 41 | #define PyInt_FromLong PyLong_FromLong 42 | #define PyInt_FromSsize_t PyLong_FromSsize_t 43 | #define PyInt_FromSize_t PyLong_FromSize_t 44 | #define PyInt_AsLong PyLong_AsLong 45 | #define PyInt_AS_LONG PyLong_AS_LONG 46 | #define PyInt_AsUnsignedLongLongMask PyLong_AsUnsignedLongLongMask 47 | #define PyInt_AsSsize_t PyLong_AsSsize_t 48 | 49 | /* Module init */ 50 | 51 | #define MODULE_INIT_FUNC(name) \ 52 | PyMODINIT_FUNC PyInit_ ## name(void); \ 53 | PyMODINIT_FUNC PyInit_ ## name(void) 54 | 55 | #else 56 | 57 | /***** Python 2 *****/ 58 | 59 | #define IS_PY3 0 60 | 61 | /* Strings */ 62 | 63 | #define PyStr_Type PyString_Type 64 | #define PyStr_Check PyString_Check 65 | #define PyStr_CheckExact PyString_CheckExact 66 | #define PyStr_FromString PyString_FromString 67 | #define PyStr_FromStringAndSize PyString_FromStringAndSize 68 | #define PyStr_FromFormat PyString_FromFormat 69 | #define PyStr_FromFormatV PyString_FromFormatV 70 | #define PyStr_AsString PyString_AsString 71 | #define PyStr_Format PyString_Format 72 | #define PyStr_InternInPlace PyString_InternInPlace 73 | #define PyStr_InternFromString PyString_InternFromString 74 | #define PyStr_Decode PyString_Decode 75 | 76 | #define PyStr_AsUTF8String(str) (Py_INCREF(str), (str)) 77 | #define PyStr_AsUTF8 PyString_AsString 78 | #define PyStr_AsUTF8AndSize(pystr, sizeptr) \ 79 | ((*sizeptr=PyString_Size(pystr)), PyString_AsString(pystr)) 80 | 81 | #define PyBytes_Type PyString_Type 82 | #define PyBytes_Check PyString_Check 83 | #define PyBytes_CheckExact PyString_CheckExact 84 | #define PyBytes_FromString PyString_FromString 85 | #define PyBytes_FromStringAndSize PyString_FromStringAndSize 86 | #define PyBytes_FromFormat PyString_FromFormat 87 | #define PyBytes_FromFormatV PyString_FromFormatV 88 | #define PyBytes_Size PyString_Size 89 | #define PyBytes_GET_SIZE PyString_GET_SIZE 90 | #define PyBytes_AsString PyString_AsString 91 | #define PyBytes_AS_STRING PyString_AS_STRING 92 | #define PyBytes_AsStringAndSize PyString_AsStringAndSize 93 | #define PyBytes_Concat PyString_Concat 94 | #define PyBytes_ConcatAndDel PyString_ConcatAndDel 95 | #define _PyBytes_Resize _PyString_Resize 96 | 97 | /* Floats */ 98 | 99 | #define PyFloat_FromString(str) PyFloat_FromString(str, NULL) 100 | 101 | /* Module init */ 102 | 103 | #define PyModuleDef_HEAD_INIT 0 104 | 105 | typedef struct PyModuleDef { 106 | int m_base; 107 | const char* m_name; 108 | const char* m_doc; 109 | Py_ssize_t m_size; 110 | PyMethodDef *m_methods; 111 | } PyModuleDef; 112 | 113 | #define PyModule_Create(def) \ 114 | Py_InitModule3((def)->m_name, (def)->m_methods, (def)->m_doc) 115 | 116 | #define MODULE_INIT_FUNC(name) \ 117 | static PyObject *PyInit_ ## name(void); \ 118 | PyMODINIT_FUNC init ## name(void); \ 119 | PyMODINIT_FUNC init ## name(void) { PyInit_ ## name(); } \ 120 | static PyObject *PyInit_ ## name(void) 121 | 122 | 123 | #endif 124 | 125 | #endif 126 | -------------------------------------------------------------------------------- /src/brainpy/_c/compat.pxd: -------------------------------------------------------------------------------- 1 | from cpython cimport PyObject 2 | 3 | cdef extern from "compat.h": 4 | char* PyStr_AsString(str string) 5 | char* PyStr_AsUTF8AndSize(str string, Py_ssize_t*) 6 | str PyStr_FromString(char* string) 7 | str PyStr_FromStringAndSize(char* string, Py_ssize_t) 8 | long PyInt_AsLong(object i) 9 | object PyInt_FromLong(long i) 10 | void PyStr_InternInPlace(PyObject** string) 11 | str PyStr_InternFromString(const char*) -------------------------------------------------------------------------------- /src/brainpy/_c/composition.pxd: -------------------------------------------------------------------------------- 1 | from brainpy.mass_dict import nist_mass as __nist_mass 2 | 3 | ctypedef long count_type 4 | 5 | cdef dict nist_mass 6 | nist_mass = __nist_mass 7 | 8 | cdef double PROTON = nist_mass["H+"][0][0] 9 | 10 | cdef double neutral_mass(double mz, int z, double charge_carrier=*) noexcept nogil 11 | cdef double mass_charge_ratio(double neutral_mass, int z, double charge_carrier=*) noexcept nogil 12 | cdef char* _parse_isotope_string(char* label, int* isotope_num, char* element_name) noexcept nogil 13 | 14 | # ----------------------------------------------------------------------------- 15 | # Isotope and IsotopeMap Declarations 16 | 17 | cdef struct Isotope: 18 | double mass 19 | double abundance 20 | int neutrons 21 | int neutron_shift 22 | 23 | cdef struct IsotopeMap: 24 | Isotope* bins 25 | size_t size 26 | 27 | cdef IsotopeMap* make_isotope_map(list organized_isotope_data, size_t size) 28 | 29 | cdef Isotope* get_isotope_by_neutron_shift(IsotopeMap* isotopes, int neutron_shift) noexcept nogil 30 | cdef void free_isotope_map(IsotopeMap* isotopes) noexcept nogil 31 | cdef void print_isotope_map(IsotopeMap* isotope_map) noexcept nogil 32 | 33 | # ----------------------------------------------------------------------------- 34 | # Element Declarations 35 | 36 | cdef struct Element: 37 | char* symbol 38 | IsotopeMap* isotopes 39 | int monoisotopic_isotope_index 40 | 41 | cdef void _isotopes_of(char* element_symbol, IsotopeMap** isotope_frequencies) 42 | cdef Element* make_element(char* symbol) 43 | 44 | cdef double element_monoisotopic_mass(Element* element) noexcept nogil 45 | cdef int element_min_neutron_shift(Element* element) noexcept nogil 46 | cdef int element_max_neutron_shift(Element* element) noexcept nogil 47 | cdef void free_element(Element* element) noexcept nogil 48 | cdef void print_element(Element* element) noexcept nogil 49 | 50 | cdef Element* make_fixed_isotope_element(Element* element, int neutron_count) noexcept nogil 51 | 52 | 53 | # ----------------------------------------------------------------------------- 54 | # ElementHashTable and ElementHashBucket Declarations 55 | 56 | cdef struct ElementHashBucket: 57 | Element** elements 58 | size_t used 59 | size_t size 60 | 61 | 62 | cdef void free_element_hash_bucket(ElementHashBucket* bucket) noexcept nogil 63 | 64 | 65 | cdef struct ElementHashTable: 66 | ElementHashBucket* buckets 67 | size_t size 68 | 69 | cdef ElementHashTable* _ElementTable 70 | 71 | cdef ElementHashTable* make_element_hash_table(size_t size) noexcept nogil 72 | 73 | cdef int element_hash_bucket_insert(ElementHashBucket* bucket, Element* element) noexcept nogil 74 | 75 | cdef int element_hash_bucket_find(ElementHashBucket* bucket, char* symbol, Element** out) noexcept nogil 76 | 77 | cdef int element_hash_table_get(ElementHashTable* table, char* symbol, Element** out) noexcept nogil 78 | 79 | cdef int element_hash_table_put(ElementHashTable* table, Element* element) noexcept nogil 80 | 81 | cdef size_t hash_string(char *str) noexcept nogil 82 | 83 | cdef size_t free_element_hash_table(ElementHashTable* table) noexcept nogil 84 | 85 | 86 | cdef ElementHashTable* get_system_element_hash_table() noexcept nogil 87 | 88 | cdef int set_system_element_hash_table(ElementHashTable* table) noexcept nogil 89 | 90 | 91 | # ----------------------------------------------------------------------------- 92 | # Composition Declarations 93 | 94 | cdef struct Composition: 95 | char** elements 96 | count_type* counts 97 | size_t size 98 | size_t used 99 | int max_variants 100 | 101 | cdef Composition* make_composition() noexcept nogil 102 | cdef Composition* copy_composition(Composition* composition) noexcept nogil 103 | cdef void print_composition(Composition* composition) noexcept nogil 104 | cdef int composition_set_element_count(Composition* composition, char* element, count_type count) noexcept nogil 105 | cdef int composition_get_element_count(Composition* composition, char* element, count_type* count) noexcept nogil 106 | cdef int composition_inc_element_count(Composition* composition, char* element, count_type increment) noexcept nogil 107 | cdef int composition_resize(Composition* composition) noexcept nogil 108 | cdef double composition_mass(Composition* composition) noexcept nogil 109 | cdef void free_composition(Composition* composition) noexcept nogil 110 | 111 | cdef Composition* composition_add(Composition* composition_1, Composition* composition_2, int sign) noexcept nogil 112 | cdef int composition_iadd(Composition* composition_1, Composition* composition_2, int sign) noexcept nogil 113 | cdef Composition* composition_mul(Composition* composition, long scale) noexcept nogil 114 | cdef void composition_imul(Composition* composition, long scale) noexcept nogil 115 | cdef int initialize_composition_from_formula(char* formula, ssize_t n, Composition* composition) noexcept nogil 116 | 117 | cdef dict composition_to_dict(Composition* composition) 118 | cdef Composition* dict_to_composition(dict comp_dict) 119 | cdef int fill_composition_from_dict(dict comp_dict, Composition* composition) except 1 120 | cdef int composition_add_from_dict(Composition* composition, dict comp_dict, int sign) except 1 121 | 122 | 123 | cdef class PyComposition(object): 124 | cdef: 125 | Composition* impl 126 | public double cached_mass 127 | public bint _clean 128 | @staticmethod 129 | cdef PyComposition _create(Composition* base) 130 | cdef void _set_impl(self, Composition* composition, bint free_existing=*) 131 | 132 | cdef void _initialize_from_formula(self, str formula) 133 | 134 | cpdef double mass(self) 135 | cpdef bint __equality_pycomposition(self, PyComposition other) 136 | cpdef bint __equality_dict(self, dict other) 137 | cpdef PyComposition copy(self) 138 | 139 | cpdef update(self, arg) 140 | cpdef list keys(self) 141 | cpdef list values(self) 142 | cpdef list items(self) 143 | 144 | cpdef pop(self, str key, object default=*) 145 | 146 | cdef count_type getitem(self, str key) 147 | cdef void setitem(self, str key, count_type value) 148 | cdef void increment(self, str key, count_type value) 149 | 150 | cdef void add_from(self, PyComposition other) 151 | cdef void subtract_from(self, PyComposition other) 152 | cdef void scale_by(self, long scale) 153 | 154 | cpdef PyComposition parse_formula(str formula) 155 | -------------------------------------------------------------------------------- /src/brainpy/_c/double_vector.pxd: -------------------------------------------------------------------------------- 1 | cdef struct DoubleVector: 2 | double* v 3 | size_t used 4 | size_t size 5 | 6 | cdef DoubleVector* make_double_vector_with_size(size_t size) noexcept nogil 7 | cdef DoubleVector* make_double_vector() noexcept nogil 8 | cdef int double_vector_resize(DoubleVector* vec) noexcept nogil 9 | cdef int double_vector_append(DoubleVector* vec, double value) noexcept nogil 10 | cdef void free_double_vector(DoubleVector* vec) noexcept nogil 11 | cdef void print_double_vector(DoubleVector* vec) noexcept nogil 12 | cdef void reset_double_vector(DoubleVector* vec) noexcept nogil 13 | cdef list double_vector_to_list(DoubleVector* vec) 14 | cdef DoubleVector* list_to_double_vector(list input_list) -------------------------------------------------------------------------------- /src/brainpy/_c/double_vector.pyx: -------------------------------------------------------------------------------- 1 | from libc.stdlib cimport malloc, realloc, free 2 | from libc cimport * 3 | 4 | cdef extern from * nogil: 5 | int printf (const char *template, ...) 6 | 7 | 8 | cdef DoubleVector* make_double_vector_with_size(size_t size) noexcept nogil: 9 | cdef: 10 | DoubleVector* vec 11 | 12 | vec = malloc(sizeof(DoubleVector)) 13 | vec.v = malloc(sizeof(double) * size) 14 | vec.size = size 15 | vec.used = 0 16 | 17 | return vec 18 | 19 | 20 | cdef DoubleVector* make_double_vector() noexcept nogil: 21 | return make_double_vector_with_size(4) 22 | 23 | 24 | cdef int double_vector_resize(DoubleVector* vec) noexcept nogil: 25 | cdef: 26 | size_t new_size 27 | double* v 28 | new_size = vec.size * 2 29 | v = realloc(vec.v, sizeof(double) * new_size) 30 | if v == NULL: 31 | printf("double_vector_resize returned -1\n") 32 | return -1 33 | vec.v = v 34 | vec.size = new_size 35 | return 0 36 | 37 | 38 | cdef int double_vector_append(DoubleVector* vec, double value) noexcept nogil: 39 | if (vec.used + 1) == vec.size: 40 | double_vector_resize(vec) 41 | vec.v[vec.used] = value 42 | vec.used += 1 43 | return 0 44 | 45 | 46 | cdef void free_double_vector(DoubleVector* vec) noexcept nogil: 47 | free(vec.v) 48 | free(vec) 49 | 50 | 51 | cdef void print_double_vector(DoubleVector* vec) noexcept nogil: 52 | cdef: 53 | size_t i 54 | i = 0 55 | printf("[") 56 | while i < vec.used: 57 | printf("%0.6f", vec.v[i]) 58 | if i != (vec.used - 1): 59 | printf(", ") 60 | i += 1 61 | printf("]\n") 62 | 63 | 64 | cdef void reset_double_vector(DoubleVector* vec) noexcept nogil: 65 | vec.used = 0 66 | 67 | 68 | cdef list double_vector_to_list(DoubleVector* vec): 69 | cdef: 70 | size_t i 71 | list result 72 | result = list() 73 | i = 0 74 | while i < vec.used: 75 | result.append(vec.v[i]) 76 | i += 1 77 | return result 78 | 79 | 80 | cdef DoubleVector* list_to_double_vector(list input_list): 81 | cdef: 82 | DoubleVector* vec 83 | double val 84 | vec = make_double_vector_with_size(len(input_list)) 85 | for val in input_list: 86 | double_vector_append(vec, val) 87 | return vec 88 | 89 | 90 | def test(): 91 | cdef list listy 92 | cdef DoubleVector* vecty 93 | 94 | listy = [1., 2., 3., 4., 23121.] 95 | vecty = list_to_double_vector(listy) 96 | print_double_vector(vecty) 97 | listy = [] 98 | print(listy) 99 | listy = double_vector_to_list(vecty) 100 | free_double_vector(vecty) 101 | print(listy) 102 | del listy 103 | -------------------------------------------------------------------------------- /src/brainpy/_c/isotopic_constants.pxd: -------------------------------------------------------------------------------- 1 | from brainpy._c.composition cimport ( 2 | _ElementTable, Element, Isotope, Composition, ElementHashTable) 3 | 4 | from brainpy._c.double_vector cimport DoubleVector 5 | 6 | ctypedef DoubleVector dvec 7 | 8 | 9 | cdef struct PolynomialParameters: 10 | dvec* elementary_symmetric_polynomial 11 | dvec* power_sum 12 | 13 | 14 | cdef struct PhiConstants: 15 | int order 16 | Element* element 17 | PolynomialParameters* element_coefficients 18 | PolynomialParameters* mass_coefficients 19 | 20 | 21 | cdef struct IsotopicConstants: 22 | int order 23 | PhiConstants** constants 24 | size_t size 25 | size_t used 26 | 27 | cdef size_t DEFAULT_ISOTOPIC_CONSTANTS_SIZE = 7 28 | 29 | 30 | cdef dvec* vietes(dvec* coefficients) noexcept nogil 31 | cdef void _update_power_sum(dvec* ps_vec, dvec* esp_vec, int order) noexcept nogil 32 | cdef void _update_elementary_symmetric_polynomial(dvec* ps_vec, dvec* esp_vec, int order) noexcept nogil 33 | cdef void newton(dvec* ps_vec, dvec* esp_vec, int order) noexcept nogil 34 | cdef dvec* compute_isotopic_coefficients(Element* element, bint with_mass, dvec* accumulator) noexcept nogil 35 | cdef PolynomialParameters* make_polynomial_parameters(Element* element, bint with_mass, dvec* accumulator) noexcept nogil 36 | cdef void print_polynomial_parameters(PolynomialParameters* params) noexcept nogil 37 | cdef void free_polynomial_parameters(PolynomialParameters* params) noexcept nogil 38 | 39 | 40 | cdef void print_phi_constants(PhiConstants* constants) noexcept nogil 41 | cdef void free_phi_constants(PhiConstants* constants) noexcept nogil 42 | 43 | 44 | cdef IsotopicConstants* make_isotopic_constants() noexcept nogil 45 | cdef int isotopic_constants_resize(IsotopicConstants* ics) noexcept nogil 46 | cdef void free_isotopic_constants(IsotopicConstants* isotopes) noexcept nogil 47 | 48 | cdef void isotopic_constants_add_element(IsotopicConstants* isotopes, char* element_symbol) noexcept nogil 49 | cdef int isotopic_constants_get(IsotopicConstants* isotopes, char* element_symbol, PhiConstants** out) noexcept nogil 50 | cdef void isotopic_constants_update_coefficients(IsotopicConstants* isotopes) noexcept nogil 51 | 52 | cdef double isotopic_constants_nth_element_power_sum(IsotopicConstants* isotopes, char* symbol, int order) noexcept nogil 53 | cdef double isotopic_constants_nth_modified_element_power_sum(IsotopicConstants* isotopes, char* symbol, int order) noexcept nogil 54 | 55 | cdef double isotopic_constants_nth_element_power_sum_by_index(IsotopicConstants* isotopes, size_t index, int order) noexcept nogil 56 | cdef double isotopic_constants_nth_modified_element_power_sum_by_index(IsotopicConstants* isotopes, size_t index, int order) noexcept nogil 57 | 58 | cdef void print_isotopic_constants(IsotopicConstants* isotopes) noexcept nogil 59 | -------------------------------------------------------------------------------- /src/brainpy/_c/isotopic_constants.pyx: -------------------------------------------------------------------------------- 1 | # cython: embedsignature=True 2 | 3 | cimport cython 4 | 5 | from brainpy._c.composition cimport ( 6 | Element, Isotope, Composition, ElementHashTable, 7 | element_max_neutron_shift, _parse_isotope_string, 8 | _ElementTable, element_hash_table_get, make_fixed_isotope_element, 9 | element_hash_table_put, make_element_hash_table, free_element_hash_table) 10 | 11 | from brainpy._c.double_vector cimport( 12 | DoubleVector, make_double_vector, double_vector_append, 13 | free_double_vector, print_double_vector, double_vector_to_list, 14 | reset_double_vector, make_double_vector_with_size) 15 | 16 | from libc.stdlib cimport malloc, free, realloc 17 | from libc.string cimport strcmp 18 | from libc cimport * 19 | 20 | cdef extern from * nogil: 21 | int printf (const char *template, ...) 22 | 23 | # ----------------------------------------------------------------------------- 24 | # PolynomialParameters Methods 25 | 26 | @cython.cdivision 27 | cdef dvec* vietes(dvec* coefficients) noexcept nogil: 28 | cdef: 29 | DoubleVector* elementary_symmetric_polynomial 30 | size_t i 31 | double tail 32 | int sign 33 | double el 34 | 35 | elementary_symmetric_polynomial = make_double_vector_with_size( 36 | coefficients.used) 37 | tail = coefficients.v[coefficients.used - 1] 38 | 39 | for i in range(coefficients.used): 40 | sign = 1 if (i % 2) == 0 else -1 41 | el = sign * coefficients.v[coefficients.used - i - 1] / tail 42 | double_vector_append(elementary_symmetric_polynomial, el) 43 | 44 | return elementary_symmetric_polynomial 45 | 46 | 47 | cdef void _update_power_sum(dvec* ps_vec, dvec* esp_vec, int order) noexcept nogil: 48 | cdef: 49 | size_t begin, end, k, j 50 | int sign 51 | double temp_ps 52 | 53 | begin = ps_vec.used 54 | end = esp_vec.used 55 | 56 | for k in range(begin, end): 57 | if k == 0: 58 | double_vector_append(ps_vec, 0.0) 59 | continue 60 | temp_ps = 0. 61 | sign = -1 62 | 63 | for j in range(1, k): 64 | sign *= -1 65 | temp_ps += sign * (esp_vec.v[j]) * (ps_vec.v[k - j]) 66 | sign *= -1 67 | temp_ps += sign * (esp_vec.v[k]) * k 68 | double_vector_append(ps_vec, temp_ps) 69 | 70 | 71 | @cython.cdivision 72 | cdef void _update_elementary_symmetric_polynomial(dvec* ps_vec, dvec* esp_vec, int order) noexcept nogil: 73 | cdef: 74 | size_t begin, end, k, j 75 | int sign 76 | double el 77 | 78 | begin = esp_vec.used 79 | end = ps_vec.used 80 | 81 | for k in range(begin, end): 82 | if k == 0: 83 | double_vector_append(esp_vec, 1.0) 84 | elif k > order: 85 | double_vector_append(esp_vec, 0.0) 86 | else: 87 | el = 0. 88 | for j in range(1, k + 1): 89 | sign = 1 if (j % 2) == 1 else -1 90 | el += sign * ps_vec.v[j] * esp_vec.v[k - j] 91 | el /= k 92 | double_vector_append(esp_vec, el) 93 | 94 | 95 | cdef void newton(dvec* ps_vec, dvec* esp_vec, int order) noexcept nogil: 96 | if ps_vec.used > esp_vec.used: 97 | _update_elementary_symmetric_polynomial(ps_vec, esp_vec, order) 98 | elif ps_vec.used < esp_vec.used: 99 | _update_power_sum(ps_vec, esp_vec, order) 100 | 101 | 102 | cdef dvec* compute_isotopic_coefficients(Element* element, bint with_mass, dvec* accumulator) noexcept nogil: 103 | cdef: 104 | int max_isotope_number, current_order 105 | Isotope* isotope 106 | double coef 107 | size_t i, j, k 108 | 109 | max_isotope_number = element_max_neutron_shift(element) 110 | for i in range(element.isotopes.size): 111 | k = element.isotopes.size - i - 1 112 | isotope = &(element.isotopes.bins[k]) 113 | current_order = max_isotope_number - isotope.neutron_shift 114 | if with_mass: 115 | coef = isotope.mass 116 | else: 117 | coef = 1.0 118 | if current_order > accumulator.used: 119 | for j in range(accumulator.used, current_order): 120 | double_vector_append(accumulator, 0.) 121 | double_vector_append(accumulator, isotope.abundance * coef) 122 | elif current_order == accumulator.used: 123 | double_vector_append(accumulator, isotope.abundance * coef) 124 | else: 125 | printf("Error, unordered isotopes for %s\n", element.symbol) 126 | return accumulator 127 | 128 | cdef PolynomialParameters* make_polynomial_parameters(Element* element, bint with_mass, dvec* accumulator) noexcept nogil: 129 | cdef: 130 | dvec* elementary_symmetric_polynomial 131 | dvec* power_sum 132 | PolynomialParameters* result 133 | 134 | compute_isotopic_coefficients(element, with_mass, accumulator) 135 | 136 | elementary_symmetric_polynomial = vietes(accumulator) 137 | power_sum = make_double_vector_with_size(elementary_symmetric_polynomial.used + 4) 138 | newton(power_sum, elementary_symmetric_polynomial, accumulator.used - 1) 139 | result = malloc(sizeof(PolynomialParameters)) 140 | result.elementary_symmetric_polynomial = elementary_symmetric_polynomial 141 | result.power_sum = power_sum 142 | return result 143 | 144 | 145 | cdef void print_polynomial_parameters(PolynomialParameters* params) noexcept nogil: 146 | printf("PolynomialParameters: %d\n", params) 147 | printf(" ") 148 | print_double_vector(params.elementary_symmetric_polynomial) 149 | printf(" ") 150 | print_double_vector(params.power_sum) 151 | printf("\n") 152 | 153 | 154 | cdef void free_polynomial_parameters(PolynomialParameters* params) noexcept nogil: 155 | free_double_vector(params.elementary_symmetric_polynomial) 156 | free_double_vector(params.power_sum) 157 | free(params) 158 | 159 | # ----------------------------------------------------------------------------- 160 | # PhiConstants Methods 161 | 162 | 163 | cdef void print_phi_constants(PhiConstants* constants) noexcept nogil: 164 | printf("PhiConstants: %d\n", constants) 165 | printf("Element: %s, Order: %d\n", constants.element.symbol, constants.order) 166 | printf("Element Coefficients:\n") 167 | print_polynomial_parameters(constants.element_coefficients) 168 | printf("Mass Coefficients:\n") 169 | print_polynomial_parameters(constants.mass_coefficients) 170 | printf("\n") 171 | 172 | 173 | cdef void free_phi_constants(PhiConstants* constants) noexcept nogil: 174 | free_polynomial_parameters(constants.element_coefficients) 175 | free_polynomial_parameters(constants.mass_coefficients) 176 | free(constants) 177 | 178 | 179 | # ----------------------------------------------------------------------------- 180 | # IsotopicConstants Methods 181 | 182 | cdef size_t DEFAULT_ISOTOPIC_CONSTANTS_SIZE = 7 183 | 184 | 185 | cdef IsotopicConstants* make_isotopic_constants() noexcept nogil: 186 | cdef: 187 | IsotopicConstants* result 188 | result = malloc(sizeof(IsotopicConstants)) 189 | result.constants = malloc(sizeof(PhiConstants*) * DEFAULT_ISOTOPIC_CONSTANTS_SIZE) 190 | result.size = DEFAULT_ISOTOPIC_CONSTANTS_SIZE 191 | result.used = 0 192 | return result 193 | 194 | 195 | cdef int isotopic_constants_resize(IsotopicConstants* ics) noexcept nogil: 196 | ics.constants = realloc(ics.constants, sizeof(PhiConstants*) * ics.size * 2) 197 | ics.size *= 2 198 | if ics.constants == NULL: 199 | return -1 200 | return 0 201 | 202 | 203 | cdef void free_isotopic_constants(IsotopicConstants* isotopes) noexcept nogil: 204 | cdef: 205 | size_t i 206 | 207 | i = 0 208 | while i < isotopes.used: 209 | free_phi_constants(isotopes.constants[i]) 210 | i += 1 211 | free(isotopes.constants) 212 | free(isotopes) 213 | 214 | 215 | cdef void isotopic_constants_add_element(IsotopicConstants* isotopes, char* element_symbol) noexcept nogil: 216 | cdef: 217 | Element* element 218 | dvec* accumulator 219 | int order, status, isotope_number 220 | PolynomialParameters* element_parameters 221 | PolynomialParameters* mass_parameters 222 | PhiConstants* phi_constants 223 | char* element_buffer 224 | 225 | status = isotopic_constants_get(isotopes, element_symbol, &phi_constants) 226 | if status == 0: 227 | return 228 | 229 | phi_constants = NULL 230 | 231 | status = element_hash_table_get(_ElementTable, element_symbol, &element) 232 | if status == -1: 233 | printf("Could not resolve element_symbol %s\n", element_symbol) 234 | return 235 | accumulator = make_double_vector() 236 | order = element_max_neutron_shift(element) 237 | element_parameters = make_polynomial_parameters(element, False, accumulator) 238 | reset_double_vector(accumulator) 239 | mass_parameters = make_polynomial_parameters(element, True, accumulator) 240 | free_double_vector(accumulator) 241 | phi_constants = malloc(sizeof(PhiConstants)) 242 | phi_constants.order = order 243 | phi_constants.element = element 244 | phi_constants.element_coefficients = element_parameters 245 | phi_constants.mass_coefficients = mass_parameters 246 | if isotopes.used + 1 == isotopes.size: 247 | isotopic_constants_resize(isotopes) 248 | isotopes.constants[isotopes.used] = phi_constants 249 | isotopes.used += 1 250 | 251 | 252 | cdef int isotopic_constants_get(IsotopicConstants* isotopes, char* element_symbol, PhiConstants** out) noexcept nogil: 253 | cdef: 254 | size_t i 255 | int status 256 | 257 | i = 0 258 | while i < isotopes.used: 259 | if strcmp(isotopes.constants[i].element.symbol, element_symbol) == 0: 260 | out[0] = (isotopes.constants[i]) 261 | return 0 262 | i += 1 263 | return 1 264 | 265 | 266 | cdef int isotopic_constants_get_by_index(IsotopicConstants* isotopes, size_t index, PhiConstants** out) noexcept nogil: 267 | if index > isotopes.used: 268 | return 1 269 | else: 270 | out[0] = isotopes.constants[index] 271 | return 0 272 | 273 | 274 | cdef void isotopic_constants_update_coefficients(IsotopicConstants* isotopes) noexcept nogil: 275 | cdef: 276 | size_t i, j 277 | PhiConstants* constants 278 | 279 | for i in range(isotopes.used): 280 | constants = isotopes.constants[i] 281 | 282 | if isotopes.order < constants.order: 283 | continue 284 | 285 | for j in range(constants.order, isotopes.order + 1): 286 | double_vector_append(constants.element_coefficients.elementary_symmetric_polynomial, 0.) 287 | double_vector_append(constants.mass_coefficients.elementary_symmetric_polynomial, 0.) 288 | 289 | constants.order = constants.element_coefficients.elementary_symmetric_polynomial.used 290 | 291 | newton(constants.element_coefficients.power_sum, 292 | constants.element_coefficients.elementary_symmetric_polynomial, 293 | constants.order) 294 | newton(constants.mass_coefficients.power_sum, 295 | constants.mass_coefficients.elementary_symmetric_polynomial, 296 | constants.order) 297 | 298 | 299 | cdef double isotopic_constants_nth_element_power_sum(IsotopicConstants* isotopes, char* symbol, int order) noexcept nogil: 300 | cdef: 301 | PhiConstants* constants 302 | isotopic_constants_get(isotopes, symbol, &constants) 303 | return constants.element_coefficients.power_sum.v[order] 304 | 305 | 306 | cdef double isotopic_constants_nth_element_power_sum_by_index(IsotopicConstants* isotopes, size_t index, int order) noexcept nogil: 307 | cdef: 308 | PhiConstants* constants 309 | isotopic_constants_get_by_index(isotopes, index, &constants) 310 | return constants.element_coefficients.power_sum.v[order] 311 | 312 | 313 | cdef double isotopic_constants_nth_modified_element_power_sum(IsotopicConstants* isotopes, char* symbol, int order) noexcept nogil: 314 | cdef: 315 | PhiConstants* constants 316 | isotopic_constants_get(isotopes, symbol, &constants) 317 | return constants.mass_coefficients.power_sum.v[order] 318 | 319 | 320 | cdef double isotopic_constants_nth_modified_element_power_sum_by_index(IsotopicConstants* isotopes, size_t index, int order) noexcept nogil: 321 | cdef: 322 | PhiConstants* constants 323 | isotopic_constants_get_by_index(isotopes, index, &constants) 324 | return constants.mass_coefficients.power_sum.v[order] 325 | 326 | 327 | cdef void print_isotopic_constants(IsotopicConstants* isotopes) noexcept nogil: 328 | cdef: 329 | size_t i 330 | 331 | i = 0 332 | for i in range(isotopes.used): 333 | printf("%d\n", i) 334 | print_phi_constants(isotopes.constants[i]) 335 | 336 | 337 | def main(): 338 | cdef: 339 | IsotopicConstants* ic 340 | dvec* coefficients 341 | dvec* elementary_symmetric_polynomial 342 | PhiConstants* constant 343 | Element* elem 344 | char* sym 345 | 346 | sym = "O" 347 | ic = make_isotopic_constants() 348 | print(ic.used) 349 | isotopic_constants_add_element(ic, sym) 350 | print(ic.used) 351 | isotopic_constants_add_element(ic, "C") 352 | isotopic_constants_add_element(ic, "H") 353 | 354 | if isotopic_constants_get(ic, "O", &constant) == 0: 355 | print_phi_constants(constant) 356 | else: 357 | print("Nope") 358 | 359 | free_isotopic_constants(ic) 360 | -------------------------------------------------------------------------------- /src/brainpy/_c/isotopic_distribution.pxd: -------------------------------------------------------------------------------- 1 | from cython cimport freelist 2 | from brainpy._c.composition cimport Composition, ElementHashTable, Element 3 | from brainpy._c.isotopic_constants cimport IsotopicConstants 4 | from brainpy._c.double_vector cimport DoubleVector 5 | 6 | 7 | cdef struct Peak: 8 | double mz 9 | double intensity 10 | int charge 11 | 12 | 13 | cdef struct PeakList: 14 | Peak* peaks 15 | size_t used 16 | size_t size 17 | 18 | 19 | cdef Peak* make_peak(double mz, double intensity, int charge) noexcept nogil 20 | cdef void print_peak(Peak* peak) noexcept nogil 21 | 22 | cdef PeakList* make_peak_list() noexcept nogil 23 | cdef void free_peak_list(PeakList* peaklist) noexcept nogil 24 | cdef int resize_peak_list(PeakList* peaklist) noexcept nogil 25 | cdef void peak_list_append(PeakList* peaklist, Peak* peak) noexcept nogil 26 | cdef void peak_list_reset(PeakList* peaklist) noexcept nogil 27 | 28 | cdef PeakList* peak_list_ignore_below(PeakList* peaklist, double ignore_below, PeakList* result) noexcept nogil 29 | cdef PeakList* peak_list_truncate_after(PeakList* peaklist, double truncate_after, PeakList* result) noexcept nogil 30 | cdef void peak_list_shift(PeakList* peaklist, double shift) noexcept nogil 31 | 32 | cdef list peaklist_to_list(PeakList* peaklist) 33 | 34 | 35 | cdef struct ElementCache: 36 | Element** elements 37 | size_t used 38 | size_t size 39 | ElementHashTable* source 40 | 41 | 42 | cdef ElementCache* make_element_cache(ElementHashTable* source) noexcept nogil 43 | cdef void free_element_cache(ElementCache* cache) noexcept nogil 44 | cdef int resize_element_cache(ElementCache* cache) noexcept nogil 45 | cdef int element_cache_put(ElementCache* cache, Element** element) noexcept nogil 46 | cdef int element_cache_get(ElementCache* cache, char* symbol, Element** out) noexcept nogil 47 | 48 | 49 | cdef struct IsotopicDistribution: 50 | Composition* composition 51 | IsotopicConstants* _isotopic_constants 52 | int order 53 | double average_mass 54 | Peak* monoisotopic_peak 55 | ElementCache* cache 56 | 57 | 58 | cdef IsotopicDistribution* make_isotopic_distribution(Composition* composition, int order, ElementCache* cache=*) noexcept nogil 59 | cdef void free_isotopic_distribution(IsotopicDistribution* distribution) noexcept nogil 60 | 61 | 62 | @freelist(100000) 63 | cdef class TheoreticalPeak(object): 64 | cdef: 65 | public double mz 66 | public double intensity 67 | public int charge 68 | 69 | @staticmethod 70 | cdef TheoreticalPeak _create(double mz, double intensity, int charge) 71 | 72 | cpdef bint _eq(self, TheoreticalPeak other) 73 | cpdef TheoreticalPeak clone(self) 74 | 75 | 76 | cdef int max_variants(Composition* composition, ElementCache* cache) noexcept nogil 77 | cdef int guess_npeaks(Composition* composition_struct, size_t max_npeaks, ElementCache* cache=*) noexcept nogil 78 | 79 | cpdef list _isotopic_variants(object composition, object npeaks=*, int charge=*, double charge_carrier=*) 80 | 81 | 82 | cdef PeakList* isotopic_variants(Composition* composition, int npeaks, int charge=*, double charge_carrier=*) noexcept nogil 83 | 84 | 85 | cdef size_t max_variants_approx(double mass, double lambda_factor=*, size_t maxiter=*, double threshold=*) noexcept nogil -------------------------------------------------------------------------------- /src/brainpy/_c/isotopic_distribution.pyx: -------------------------------------------------------------------------------- 1 | # cython: embedsignature=True 2 | # -*- coding: utf-8 -*- 3 | cimport cython 4 | 5 | from brainpy._c.composition cimport ( 6 | Element, Isotope, Composition, ElementHashTable, 7 | mass_charge_ratio, PROTON, element_max_neutron_shift, 8 | composition_get_element_count, element_monoisotopic_mass, 9 | composition_mass, get_isotope_by_neutron_shift, dict_to_composition, 10 | print_composition, free_composition, count_type, 11 | make_element_hash_table, free_element_hash_table, 12 | _ElementTable, element_hash_table_get, make_fixed_isotope_element, 13 | _parse_isotope_string, element_hash_table_put) 14 | 15 | from brainpy._c.double_vector cimport( 16 | DoubleVector, make_double_vector, double_vector_append, 17 | make_double_vector_with_size, 18 | free_double_vector, print_double_vector, reset_double_vector) 19 | 20 | from libc.stdlib cimport malloc, free, realloc 21 | from libc.string cimport strcmp 22 | from libc.math cimport log, exp, sqrt, fabs 23 | from libc cimport * 24 | 25 | from brainpy._c.isotopic_constants cimport ( 26 | IsotopicConstants, isotopic_constants_get, make_isotopic_constants, 27 | isotopic_constants_resize, free_isotopic_constants, isotopic_constants_add_element, 28 | isotopic_constants_update_coefficients, 29 | isotopic_constants_nth_element_power_sum, print_isotopic_constants, 30 | isotopic_constants_nth_element_power_sum_by_index, 31 | isotopic_constants_nth_modified_element_power_sum, 32 | newton) 33 | 34 | 35 | # from double_vector cimport ( 36 | # DoubleVector, free_double_vector, make_double_vector, 37 | # double_vector_append, print_double_vector) 38 | 39 | ctypedef DoubleVector dvec 40 | 41 | 42 | cdef extern from * nogil: 43 | int printf (const char *template, ...) 44 | void qsort (void *base, unsigned short n, unsigned short w, int (*cmp_func)(void*, void*)) 45 | 46 | 47 | from libc.math cimport isinf as isinf 48 | 49 | # ----------------------------------------------------------------------------- 50 | # ElementPolynomialMap Declaration and Methods 51 | 52 | cdef struct ElementPolynomialMap: 53 | char** elements 54 | dvec** polynomials 55 | size_t used 56 | size_t size 57 | 58 | cdef ElementPolynomialMap* make_element_polynomial_map(size_t sizehint) noexcept nogil: 59 | cdef ElementPolynomialMap* result 60 | result = malloc(sizeof(ElementPolynomialMap)) 61 | result.elements = malloc(sizeof(char*) * sizehint) 62 | result.polynomials = malloc(sizeof(dvec*) * sizehint) 63 | result.size = sizehint 64 | result.used = 0 65 | 66 | return result 67 | 68 | cdef int element_polynomial_map_set(ElementPolynomialMap* ep_map, char* element, dvec* polynomial) noexcept nogil: 69 | cdef: 70 | size_t i 71 | int status 72 | bint done 73 | 74 | done = False 75 | i = 0 76 | while (i < ep_map.used): 77 | if strcmp(element, ep_map.elements[i]) == 0: 78 | done = True 79 | ep_map.polynomials[i] = polynomial 80 | return 0 81 | i += 1 82 | if not done: 83 | ep_map.used += 1 84 | if ep_map.used >= ep_map.size: 85 | printf("Overloaded ElementPolynomialMap\n %d, %d\n", i, ep_map.size) 86 | return -1 87 | ep_map.elements[i] = element 88 | ep_map.polynomials[i] = polynomial 89 | return 0 90 | return 1 91 | 92 | cdef int element_polynomial_map_get(ElementPolynomialMap* ep_map, char* element, dvec** polynomial) noexcept nogil: 93 | cdef: 94 | size_t i 95 | int status 96 | bint done 97 | 98 | done = False 99 | i = 0 100 | while i < ep_map.used: 101 | if strcmp(ep_map.elements[i], element) == 0: 102 | polynomial[0] = ep_map.polynomials[i] 103 | return 0 104 | i += 1 105 | return 1 106 | 107 | cdef void free_element_polynomial_map(ElementPolynomialMap* ep_map) noexcept nogil: 108 | cdef: 109 | size_t i 110 | i = 0 111 | while i < ep_map.used: 112 | free_double_vector(ep_map.polynomials[i]) 113 | i += 1 114 | free(ep_map.elements) 115 | free(ep_map.polynomials) 116 | free(ep_map) 117 | 118 | 119 | # ----------------------------------------------------------------------------- 120 | # Peak Methods 121 | 122 | cdef void print_peak(Peak* peak) noexcept nogil: 123 | printf("Peak: %f, %f, %d\n", peak.mz, peak.intensity, peak.charge) 124 | 125 | 126 | cdef Peak* make_peak(double mz, double intensity, int charge) noexcept nogil: 127 | cdef Peak* peak 128 | peak = malloc(sizeof(Peak)) 129 | peak.mz = mz 130 | peak.intensity = intensity 131 | peak.charge = charge 132 | return peak 133 | 134 | # ----------------------------------------------------------------------------- 135 | # PeakList Methods 136 | 137 | cdef PeakList* make_peak_list() noexcept nogil: 138 | cdef PeakList* result 139 | 140 | result = malloc(sizeof(PeakList)) 141 | result.peaks = malloc(sizeof(Peak) * 10) 142 | result.size = 10 143 | result.used = 0 144 | 145 | return result 146 | 147 | cdef void free_peak_list(PeakList* peaklist) noexcept nogil: 148 | free(peaklist.peaks) 149 | free(peaklist) 150 | 151 | cdef int resize_peak_list(PeakList* peaklist) noexcept nogil: 152 | cdef: 153 | Peak* peaks 154 | peaks = realloc(peaklist.peaks, sizeof(Peak) * peaklist.size * 2) 155 | if peaks == NULL: 156 | printf("realloc peaklist returned NULL\n") 157 | return -1 158 | peaklist.peaks = peaks 159 | peaklist.size *= 2 160 | return 0 161 | 162 | cdef void peak_list_append(PeakList* peaklist, Peak* peak) noexcept nogil: 163 | if peaklist.used == peaklist.size: 164 | resize_peak_list(peaklist) 165 | peaklist.peaks[peaklist.used] = peak[0] 166 | peaklist.used += 1 167 | 168 | cdef void peak_list_reset(PeakList* peaklist) noexcept nogil: 169 | peaklist.used = 0 170 | 171 | @cython.cdivision 172 | cdef PeakList* peak_list_ignore_below(PeakList* peaklist, double ignore_below, PeakList* result) noexcept nogil: 173 | cdef: 174 | double total 175 | PeakList* kept_tid 176 | size_t i, n 177 | Peak p 178 | 179 | total = 0 180 | n = peaklist.used 181 | 182 | if result == NULL: 183 | result = make_peak_list() 184 | 185 | for i in range(n): 186 | p = result.peaks[i] 187 | if (p.intensity < ignore_below) and (i > 1): 188 | continue 189 | else: 190 | total += p.intensity 191 | peak_list_append(result, &p) 192 | n = result.used 193 | for i in range(n): 194 | result.peaks[i].intensity /= total 195 | return result 196 | 197 | @cython.cdivision 198 | cdef PeakList* peak_list_truncate_after(PeakList* peaklist, double truncate_after, PeakList* result) noexcept nogil: 199 | cdef: 200 | double cumsum 201 | Peak peak 202 | size_t i, n 203 | 204 | cumsum = 0 205 | n = peaklist.used 206 | if result == NULL: 207 | result = make_peak_list() 208 | 209 | for i in range(n): 210 | peak = peaklist.peaks[i] 211 | cumsum += peak.intensity 212 | peak_list_append(result, &peak) 213 | if cumsum >= truncate_after: 214 | break 215 | 216 | n = result.used 217 | for i in range(n): 218 | result.peaks[i].intensity /= cumsum 219 | return result 220 | 221 | cdef void peak_list_shift(PeakList* peaklist, double shift) noexcept nogil: 222 | cdef: 223 | size_t i, n 224 | double delta 225 | 226 | n = peaklist.used 227 | if n == 0: 228 | return 229 | delta = shift - peaklist.peaks[0].mz 230 | for i in range(n): 231 | peaklist.peaks[i].mz += delta 232 | 233 | # ----------------------------------------------------------------------------- 234 | # ElementCache Methods 235 | 236 | cdef ElementCache* make_element_cache(ElementHashTable* source) noexcept nogil: 237 | cdef: 238 | ElementCache* cache 239 | cache = malloc(sizeof(ElementCache)) 240 | cache.source = source 241 | cache.elements = malloc(sizeof(Element*) * 10) 242 | cache.used = 0 243 | cache.size = 10 244 | return cache 245 | 246 | 247 | cdef void free_element_cache(ElementCache* cache) noexcept nogil: 248 | free(cache.elements) 249 | free(cache) 250 | 251 | 252 | cdef int resize_element_cache(ElementCache* cache) noexcept nogil: 253 | cdef: 254 | Element** values 255 | size_t new_size 256 | new_size = cache.size * 10 257 | values = realloc(cache.elements, sizeof(Element*) * new_size) 258 | if values == NULL: 259 | printf("resize_element_cache returned -1\n") 260 | return -1 261 | cache.elements = values 262 | cache.size = new_size 263 | return 0 264 | 265 | 266 | cdef int element_cache_put(ElementCache* cache, Element** element) noexcept nogil: 267 | cdef: 268 | size_t i 269 | if (cache.used + 1) == cache.size: 270 | resize_element_cache(cache) 271 | cache.elements[cache.used] = element[0] 272 | cache.used += 1 273 | return 0 274 | 275 | 276 | cdef int element_cache_get(ElementCache* cache, char* symbol, Element** out) noexcept nogil: 277 | cdef: 278 | size_t i 279 | Element* element 280 | 281 | if cache == NULL: 282 | return element_hash_table_get(_ElementTable, symbol, out) 283 | 284 | for i in range(cache.used): 285 | element = cache.elements[i] 286 | if strcmp(element.symbol, symbol) == 0: 287 | out[0] = element 288 | return 0 289 | element_hash_table_get(cache.source, symbol, out) 290 | element_cache_put(cache, out) 291 | return 1 292 | 293 | 294 | # ----------------------------------------------------------------------------- 295 | # Support Functions 296 | 297 | cdef int max_variants(Composition* composition, ElementCache* cache) noexcept nogil: 298 | cdef: 299 | size_t i 300 | int max_variants 301 | double count 302 | char* symbol 303 | Element* element 304 | 305 | if composition.max_variants != 0: 306 | return composition.max_variants 307 | 308 | max_variants = 0 309 | for i in range(composition.used): 310 | symbol = composition.elements[i] 311 | count = composition.counts[i] 312 | if cache == NULL: 313 | element_hash_table_get(_ElementTable, symbol, &element) 314 | else: 315 | element_cache_get(cache, symbol, &element) 316 | max_variants += (count) * element_max_neutron_shift(element) 317 | composition.max_variants = max_variants 318 | return max_variants 319 | 320 | 321 | cdef int validate_composition(Composition* composition) noexcept nogil: 322 | cdef: 323 | size_t i 324 | char* element_symbol 325 | char* element_buffer 326 | int status, isotope_number 327 | Element* element 328 | int retcode 329 | 330 | retcode = 0 331 | for i in range(composition.used): 332 | element_symbol = composition.elements[i] 333 | status = element_hash_table_get(_ElementTable, element_symbol, &element) 334 | # printf("Element %s, Status %d\n", element_symbol, status) 335 | if status == -1: 336 | element_buffer = malloc(sizeof(char) * 10) 337 | # printf("Could not resolve element %s, attempting to generate fixed isotope\n", element_symbol) 338 | _parse_isotope_string(element_symbol, &isotope_number, element_buffer) 339 | # printf("Element: %s, Isotope: %d\n", element_buffer, isotope_number) 340 | if isotope_number != 0: 341 | status = element_hash_table_get(_ElementTable, element_buffer, &element) 342 | # printf("Retreived Base Element: %s. Status: %d\n", element_buffer, status) 343 | element = make_fixed_isotope_element(element, isotope_number) 344 | if element == NULL: 345 | retcode = 1 346 | free(element_buffer) 347 | return retcode 348 | element_hash_table_put(_ElementTable, element) 349 | free(element_buffer) 350 | else: 351 | # printf("Could not resolve element %s\n", element_symbol) 352 | free(element_buffer) 353 | return retcode 354 | 355 | # ----------------------------------------------------------------------------- 356 | # IsotopicDistribution Methods 357 | 358 | cdef void isotopic_distribution_update_isotopic_constants(IsotopicDistribution* distribution) noexcept nogil: 359 | cdef: 360 | size_t i 361 | Composition* composition 362 | IsotopicConstants* isotopes 363 | char* symbol 364 | 365 | composition = distribution.composition 366 | isotopes = distribution._isotopic_constants 367 | 368 | for i in range(composition.used): 369 | symbol = composition.elements[i] 370 | isotopic_constants_add_element(isotopes, symbol) 371 | isotopes.order = distribution.order 372 | isotopic_constants_update_coefficients(isotopes) 373 | 374 | 375 | cdef void isotopic_distribution_update_order(IsotopicDistribution* distribution, int order) noexcept nogil: 376 | cdef: 377 | int possible_variants 378 | 379 | possible_variants = max_variants(distribution.composition, NULL) 380 | if order == -1: 381 | distribution.order = possible_variants 382 | else: 383 | distribution.order = min(order, possible_variants) 384 | 385 | isotopic_distribution_update_isotopic_constants(distribution) 386 | 387 | 388 | cdef Peak* isotopic_distribution_make_monoisotopic_peak(IsotopicDistribution* distribution) noexcept nogil: 389 | cdef: 390 | Peak* peak 391 | size_t i 392 | Element* element 393 | int status 394 | double intensity 395 | 396 | peak = malloc(sizeof(Peak)) 397 | peak.mz = composition_mass(distribution.composition) 398 | 399 | intensity = 0 400 | for i in range(distribution.composition.used): 401 | status = element_cache_get(distribution.cache, distribution.composition.elements[i], &element) 402 | intensity += log(get_isotope_by_neutron_shift(element.isotopes, 0).abundance) 403 | intensity = exp(intensity) 404 | peak.intensity = intensity 405 | peak.charge = 0 406 | return peak 407 | 408 | 409 | cdef IsotopicDistribution* make_isotopic_distribution(Composition* composition, int order, ElementCache* cache=NULL) noexcept nogil: 410 | cdef: 411 | IsotopicDistribution* result 412 | if cache == NULL: 413 | cache = make_element_cache(_ElementTable) 414 | result = malloc(sizeof(IsotopicDistribution)) 415 | result.composition = composition 416 | result.cache = cache 417 | result.order = 0 418 | result._isotopic_constants = make_isotopic_constants() 419 | isotopic_distribution_update_order(result, order) 420 | result.average_mass = 0 421 | result.monoisotopic_peak = isotopic_distribution_make_monoisotopic_peak(result) 422 | return result 423 | 424 | 425 | cdef void free_isotopic_distribution(IsotopicDistribution* distribution) noexcept nogil: 426 | free(distribution.monoisotopic_peak) 427 | free_isotopic_constants(distribution._isotopic_constants) 428 | if distribution.cache != NULL: 429 | free_element_cache(distribution.cache) 430 | free(distribution) 431 | 432 | 433 | cdef dvec* id_phi_values(IsotopicDistribution* distribution) noexcept nogil: 434 | cdef: 435 | dvec* power_sum 436 | size_t i 437 | power_sum = make_double_vector_with_size(distribution.order) 438 | double_vector_append(power_sum, 0.) 439 | for i in range(1, distribution.order + 1): 440 | double_vector_append(power_sum, _id_phi_value(distribution, i)) 441 | return power_sum 442 | 443 | 444 | cdef double _id_phi_value(IsotopicDistribution* distribution, int order) noexcept nogil: 445 | cdef: 446 | double phi 447 | char* element 448 | double count 449 | size_t i 450 | 451 | phi = 0 452 | i = 0 453 | while i < distribution.composition.used: 454 | element = distribution.composition.elements[i] 455 | count = distribution.composition.counts[i] 456 | phi += isotopic_constants_nth_element_power_sum( 457 | distribution._isotopic_constants, element, order) * count 458 | i += 1 459 | return phi 460 | 461 | 462 | cdef dvec* id_modified_phi_values(IsotopicDistribution* distribution, char* element, dvec* power_sum) noexcept nogil: 463 | cdef: 464 | size_t i 465 | 466 | double_vector_append(power_sum, 0.) 467 | for i in range(1, distribution.order + 1): 468 | double_vector_append(power_sum, 469 | _id_modified_phi_value(distribution, element, i)) 470 | return power_sum 471 | 472 | 473 | cdef double _id_modified_phi_value(IsotopicDistribution* distribution, char* symbol, int order) noexcept nogil: 474 | cdef: 475 | double phi 476 | char* element 477 | double count 478 | size_t i 479 | double coef 480 | 481 | phi = 0 482 | i = 0 483 | while i < distribution.composition.used: 484 | element = distribution.composition.elements[i] 485 | count = distribution.composition.counts[i] 486 | 487 | if strcmp(element, symbol) == 0: 488 | coef = count - 1 489 | else: 490 | coef = count 491 | 492 | phi += isotopic_constants_nth_element_power_sum( 493 | distribution._isotopic_constants, element, order) * coef 494 | i += 1 495 | 496 | phi += isotopic_constants_nth_modified_element_power_sum( 497 | distribution._isotopic_constants, symbol, order) 498 | 499 | return phi 500 | 501 | 502 | cdef dvec* id_probability_vector(IsotopicDistribution* distribution) noexcept nogil: 503 | cdef: 504 | dvec* phi_values 505 | int max_variant_count 506 | dvec* probability_vector 507 | size_t i 508 | int sign 509 | 510 | phi_values = id_phi_values(distribution) 511 | probability_vector = make_double_vector_with_size(phi_values.size) 512 | max_variant_count = distribution.order + 1 513 | 514 | newton(phi_values, probability_vector, max_variant_count) 515 | 516 | for i in range(0, probability_vector.used): 517 | sign = 1 if i % 2 == 0 else -1 518 | probability_vector.v[i] *= distribution.monoisotopic_peak.intensity * sign 519 | 520 | free_double_vector(phi_values) 521 | return probability_vector 522 | 523 | 524 | @cython.cdivision 525 | cdef dvec* id_center_mass_vector(IsotopicDistribution* distribution, dvec* probability_vector) noexcept nogil: 526 | cdef: 527 | dvec* mass_vector 528 | dvec* power_sum 529 | dvec* ele_sym_poly 530 | int max_variant_count, sign 531 | Element* element_struct 532 | char* element 533 | count_type _element_count 534 | double center, temp, polynomial_term 535 | double _monoisotopic_mass, base_intensity 536 | size_t i, j, k 537 | ElementPolynomialMap* ep_map 538 | 539 | mass_vector = make_double_vector_with_size(probability_vector.size + 3) 540 | power_sum = make_double_vector() 541 | max_variant_count = distribution.order + 1 542 | base_intensity = distribution.monoisotopic_peak.intensity 543 | ep_map = make_element_polynomial_map(distribution.composition.size) 544 | 545 | j = 0 546 | while j < distribution.composition.used: 547 | element = distribution.composition.elements[j] 548 | reset_double_vector(power_sum) 549 | power_sum = id_modified_phi_values(distribution, element, power_sum) 550 | ele_sym_poly = make_double_vector() 551 | newton(power_sum, ele_sym_poly, max_variant_count) 552 | element_polynomial_map_set(ep_map, element, ele_sym_poly) 553 | j += 1 554 | free_double_vector(power_sum) 555 | i = 0 556 | for i in range(distribution.order + 1): 557 | sign = 1 if i % 2 == 0 else -1 558 | center = 0.0 559 | k = 0 560 | while k < ep_map.used: 561 | element = ep_map.elements[k] 562 | element_polynomial_map_get(ep_map, element, &ele_sym_poly) 563 | 564 | composition_get_element_count(distribution.composition, element, &_element_count) 565 | element_cache_get(distribution.cache, element, &element_struct) 566 | 567 | _monoisotopic_mass = element_monoisotopic_mass(element_struct) 568 | 569 | polynomial_term = ele_sym_poly.v[i] 570 | temp = _element_count 571 | temp *= sign * polynomial_term 572 | temp *= base_intensity * _monoisotopic_mass 573 | center += temp 574 | k += 1 575 | if probability_vector.v[i] == 0: 576 | double_vector_append(mass_vector, 0) 577 | else: 578 | double_vector_append(mass_vector, center / probability_vector.v[i]) 579 | 580 | free_element_polynomial_map(ep_map) 581 | 582 | return mass_vector 583 | 584 | 585 | @cython.cdivision 586 | cdef PeakList* id_aggregated_isotopic_variants(IsotopicDistribution* distribution, int charge, double charge_carrier) noexcept nogil: 587 | cdef: 588 | dvec* probability_vector 589 | dvec* center_mass_vector 590 | PeakList* peak_set 591 | double average_mass, adjusted_mz 592 | double total 593 | size_t i 594 | Peak peak 595 | double center_mass_i, intensity_i 596 | 597 | probability_vector = id_probability_vector(distribution) 598 | center_mass_vector = id_center_mass_vector(distribution, probability_vector) 599 | 600 | peak_set = make_peak_list() 601 | 602 | average_mass = 0 603 | total = 0 604 | 605 | for i in range(probability_vector.used): 606 | total += probability_vector.v[i] 607 | 608 | for i in range(distribution.order + 1): 609 | center_mass_i = center_mass_vector.v[i] 610 | intensity_i = probability_vector.v[i] 611 | 612 | 613 | if charge != 0: 614 | adjusted_mz = mass_charge_ratio(center_mass_i, charge, charge_carrier) 615 | else: 616 | adjusted_mz = center_mass_i 617 | 618 | 619 | peak.mz = adjusted_mz 620 | peak.intensity = intensity_i / total 621 | peak.charge = charge 622 | 623 | if peak.intensity < 1e-10: 624 | continue 625 | 626 | peak_list_append(peak_set, &peak) 627 | 628 | average_mass += adjusted_mz * intensity_i 629 | 630 | average_mass /= total 631 | 632 | free_double_vector(probability_vector) 633 | free_double_vector(center_mass_vector) 634 | 635 | distribution.average_mass = average_mass 636 | 637 | sort_by_mz(peak_set) 638 | return peak_set 639 | 640 | cdef void sort_by_mz(PeakList* peaklist) noexcept nogil: 641 | qsort(peaklist.peaks, peaklist.used, sizeof(Peak), compare_by_mz) 642 | 643 | cdef int compare_by_mz(const void * a, const void * b) noexcept nogil: 644 | if (a).mz < (b).mz: 645 | return -1 646 | elif (a).mz == (b).mz: 647 | return 0 648 | elif (a).mz > (b).mz: 649 | return 1 650 | 651 | 652 | cpdef bint check_composition_non_negative(dict composition): 653 | cdef: 654 | bint negative_element 655 | str k 656 | object v 657 | 658 | negative_element = False 659 | for k, v in composition.items(): 660 | if v < 0: 661 | negative_element = True 662 | break 663 | return negative_element 664 | 665 | 666 | cdef int guess_npeaks(Composition* composition_struct, size_t max_npeaks, ElementCache* cache=NULL) noexcept nogil: 667 | cdef: 668 | int max_n_variants, npeaks 669 | max_n_variants = max_variants(composition_struct, cache) 670 | npeaks = sqrt(max_n_variants) - 2 671 | npeaks = min(max(npeaks, 3), max_npeaks) 672 | return npeaks 673 | 674 | 675 | def pyisotopic_variants(object composition not None, object npeaks=None, int charge=0, 676 | double charge_carrier=PROTON): 677 | """ 678 | Compute a peak list representing the theoretical isotopic cluster for `composition`. 679 | 680 | Parameters 681 | ---------- 682 | composition : Mapping 683 | Any Mapping type where keys are element symbols and values are integers. Elements may be fixed 684 | isotopes where their isotope number is enclosed in square braces (e.g. "C[13]"). Fixed isotopes 685 | that are not recognized will throw an error. 686 | n_peaks: int 687 | The number of peaks to include in the isotopic cluster, starting from the monoisotopic peak. 688 | If given a number below 1 or above the maximum number of isotopic variants, the maximum will 689 | be used. If `None`, a "reasonable" value is chosen by `int(sqrt(max_variants(composition)))`. 690 | charge: int 691 | The charge state of the isotopic cluster to produce. Defaults to 0, theoretical neutral mass. 692 | charge_carrier: double 693 | The mass of the molecule contributing the ion's charge 694 | 695 | Returns 696 | ------- 697 | list of TheoreticalPeak 698 | """ 699 | return _isotopic_variants(composition, npeaks, charge, charge_carrier) 700 | 701 | 702 | cpdef list _isotopic_variants(object composition, object npeaks=None, int charge=0, double charge_carrier=PROTON): 703 | """ 704 | Compute a peak list representing the theoretical isotopic cluster for `composition`. 705 | 706 | Parameters 707 | ---------- 708 | composition : Mapping 709 | Any Mapping type where keys are element symbols and values are integers. Elements may be fixed 710 | isotopes where their isotope number is enclosed in square braces (e.g. "C[13]"). Fixed isotopes 711 | that are not recognized will throw an error. 712 | n_peaks: int 713 | The number of peaks to include in the isotopic cluster, starting from the monoisotopic peak. 714 | If given a number below 1 or above the maximum number of isotopic variants, the maximum will 715 | be used. If `None`, a "reasonable" value is chosen by `int(sqrt(max_variants(composition)))`. 716 | charge: int 717 | The charge state of the isotopic cluster to produce. Defaults to 0, theoretical neutral mass. 718 | charge_carrier: double 719 | The mass of the molecule contributing the ion's charge 720 | 721 | Returns 722 | ------- 723 | list of TheoreticalPeak 724 | """ 725 | cdef: 726 | Composition* composition_struct 727 | list peaklist 728 | PeakList* peak_set 729 | IsotopicDistribution* dist 730 | int _npeaks, max_n_variants 731 | 732 | composition_struct = dict_to_composition(dict(composition)) 733 | if validate_composition(composition_struct) != 0: 734 | free_composition(composition_struct) 735 | raise KeyError("Unrecognized Isotope") 736 | 737 | if npeaks is None: 738 | _npeaks = guess_npeaks(composition_struct, 300) 739 | else: 740 | # The npeaks variable is left as a Python-level variable to 741 | # allow it to be any Python numeric type 742 | _npeaks = npeaks - 1 743 | 744 | with nogil: 745 | dist = make_isotopic_distribution(composition_struct, _npeaks) 746 | peak_set = id_aggregated_isotopic_variants(dist, charge, charge_carrier) 747 | 748 | peaklist = peaklist_to_list(peak_set) 749 | 750 | free_peak_list(peak_set) 751 | free_isotopic_distribution(dist) 752 | free_composition(composition_struct) 753 | return peaklist 754 | 755 | 756 | cdef PeakList* isotopic_variants(Composition* composition, int npeaks, int charge=0, double charge_carrier=PROTON) noexcept nogil: 757 | cdef: 758 | IsotopicDistribution* dist 759 | PeakList* peaklist 760 | int max_n_variants 761 | 762 | if validate_composition(composition) != 0: 763 | printf("Malformed composition\n") 764 | return NULL 765 | 766 | if npeaks == 0: 767 | npeaks = guess_npeaks(composition, 300) 768 | else: 769 | npeaks = npeaks - 1 770 | 771 | dist = make_isotopic_distribution(composition, npeaks) 772 | peaklist = id_aggregated_isotopic_variants(dist, charge, charge_carrier) 773 | free_isotopic_distribution(dist) 774 | 775 | return peaklist 776 | 777 | 778 | cdef list peaklist_to_list(PeakList* peaklist): 779 | cdef: 780 | list pypeaklist 781 | size_t i 782 | pypeaklist = [] 783 | for i in range(peaklist.used): 784 | pypeaklist.append(TheoreticalPeak._create( 785 | peaklist.peaks[i].mz, peaklist.peaks[i].intensity, peaklist.peaks[i].charge)) 786 | return pypeaklist 787 | 788 | 789 | @cython.freelist(1000000) 790 | cdef class TheoreticalPeak(object): 791 | def __init__(self, mz, intensity, charge): 792 | self.mz = mz 793 | self.intensity = intensity 794 | self.charge = charge 795 | 796 | def __repr__(self): 797 | return "Peak(mz=%f, intensity=%f, charge=%d)" % (self.mz, self.intensity, self.charge) 798 | 799 | cpdef bint _eq(self, TheoreticalPeak other): 800 | cdef bint val 801 | val = (abs(self.mz - other.mz) < 1e-10 and\ 802 | abs(self.intensity - other.intensity) < 1e-10 and\ 803 | self.charge == other.charge) 804 | return val 805 | 806 | def __hash__(self): 807 | return hash((self.mz, self.intensity, self.charge)) 808 | 809 | def __richcmp__(self, other, int code): 810 | if code == 2: 811 | return self._eq(other) 812 | elif code == 3: 813 | return not self._eq(other) 814 | 815 | def __reduce__(self): 816 | return TheoreticalPeak, (self.mz, self.intensity, self.charge) 817 | 818 | cpdef TheoreticalPeak clone(self): 819 | return TheoreticalPeak._create(self.mz, self.intensity, self.charge) 820 | 821 | @staticmethod 822 | cdef TheoreticalPeak _create(double mz, double intensity, int charge): 823 | cdef: 824 | TheoreticalPeak inst 825 | inst = TheoreticalPeak.__new__(TheoreticalPeak) 826 | inst.mz = mz 827 | inst.intensity = intensity 828 | inst.charge = charge 829 | return inst 830 | 831 | 832 | def main(): 833 | cdef: 834 | dict comp_dict 835 | Composition* composition 836 | IsotopicDistribution* distribution 837 | IsotopicDistribution* distribution2 838 | 839 | comp_dict = dict(H=2, O=1) 840 | print(comp_dict) 841 | composition = dict_to_composition(comp_dict) 842 | print_composition(composition) 843 | distribution = make_isotopic_distribution(composition, 4) 844 | print("Going to print constants") 845 | print_isotopic_constants(distribution._isotopic_constants) 846 | print("Done") 847 | 848 | print("Trying to free") 849 | free_isotopic_distribution(distribution) 850 | print( "Free Done") 851 | 852 | distribution2 = make_isotopic_distribution(composition, 4) 853 | print( "Seconc construction") 854 | print_isotopic_constants(distribution2._isotopic_constants) 855 | print( "Second Free") 856 | free_isotopic_distribution(distribution2) 857 | print(comp_dict) 858 | free_composition(composition) 859 | print( "Really done") 860 | 861 | 862 | def test(object composition, int max_npeaks=300): 863 | cdef: 864 | Composition* composition_struct 865 | IsotopicDistribution* distribution 866 | int npeaks, max_n_variants 867 | 868 | composition_struct = dict_to_composition(dict(composition)) 869 | validate_composition(composition_struct) 870 | 871 | npeaks = guess_npeaks(composition_struct, max_npeaks) 872 | print("Guessed # of Peaks: ", npeaks) 873 | 874 | dist = make_isotopic_distribution(composition_struct, npeaks) 875 | 876 | print("Size of probability vector:", id_probability_vector(dist).used) 877 | 878 | peak_set = id_aggregated_isotopic_variants(dist, 1, PROTON) 879 | 880 | peaklist = peaklist_to_list(peak_set) 881 | 882 | print("Actual # of Peaks Returned:", len(peaklist)) 883 | 884 | free_peak_list(peak_set) 885 | free_isotopic_distribution(dist) 886 | free_composition(composition_struct) 887 | return peaklist 888 | 889 | 890 | cdef size_t max_variants_approx(double mass, double lambda_factor=1800.0, size_t maxiter=255, 891 | double threshold=0.9999) noexcept nogil: 892 | """Approximate the maximum number of isotopic peaks to include in an isotopic distribution 893 | approximation for biomolecules using the Poisson distribution, using the method described 894 | in Bellew et al [1]. 895 | 896 | This algorithm adds peaks to the approximated isotopic pattern until `threshold`% signal 897 | is generated. 898 | 899 | Parameters 900 | ----------- 901 | mass : double 902 | The mass generate th approximate pattern for. 903 | lambda_factor : double 904 | The initial Poisson parameter. The default value is taken from the original 905 | reference [1] 906 | maxiter : int 907 | The maximum number of iterations to run for, also the maximum number of peaks 908 | that can be added. This approximation becomes more and more inaccurate the 909 | larger the value gets. 910 | threshold : double 911 | The percentage of the total signal to collect until terminating. 912 | 913 | Returns 914 | ------- 915 | n_peaks : int 916 | The number of isotopic peaks to generate, or exceeded the `maxiter` if 0 917 | 918 | References 919 | ---------- 920 | [1] Bellew, M., Coram, M., Fitzgibbon, M., Igra, M., Randolph, T., Wang, P., May, D., Eng, J., Fang, R., Lin, C., Chen, J., 921 | Goodlett, D., Whiteaker, J., Paulovich, A., & Mcintosh, M. (2006). A suite of algorithms for the comprehensive analysis 922 | of complex protein mixtures using high-resolution LC-MS. 22(15), 1902–1909. https://doi.org/10.1093/bioinformatics/btl276 923 | """ 924 | cdef: 925 | double lmbda = mass / lambda_factor 926 | double p_i = 1.0 927 | double factorial_acc = 1.0 928 | double acc = 1.0 929 | double cur_intensity 930 | size_t i 931 | 932 | threshold = 1.0 - threshold 933 | for i in range(1, maxiter): 934 | p_i *= lmbda 935 | factorial_acc *= i 936 | cur_intensity = p_i / factorial_acc 937 | if isinf(cur_intensity): 938 | return i 939 | acc += cur_intensity 940 | if cur_intensity / acc < threshold: 941 | return i 942 | return 0 943 | 944 | 945 | def py_max_variants_approx(double mass, double lambda_factor=1800.0, size_t maxiter=255, 946 | double threshold=0.9999): 947 | with nogil: 948 | val = max_variants_approx(mass, lambda_factor, maxiter, threshold) 949 | return val -------------------------------------------------------------------------------- /src/brainpy/_speedup.pxd: -------------------------------------------------------------------------------- 1 | cdef dict nist_mass 2 | cdef double PROTON 3 | cdef dict periodic_table 4 | 5 | 6 | cdef double neutral_mass(double mz, int z, double charge_carrier=*) 7 | cdef double mass_charge_ratio(double neutral_mass, int z, double charge_carrier=*) 8 | 9 | cdef double calculate_mass(dict composition, dict mass_data=*) 10 | 11 | cpdef Element make_fixed_isotope_element(Element element, int isotope_shift) 12 | 13 | cdef class Element: 14 | cdef: 15 | public str symbol 16 | public dict isotopes 17 | double _monoisotopic_mass 18 | int _max_neutron_shift 19 | int _min_neutron_shift 20 | list _no_mass_elementary_symmetric_polynomial_cache 21 | list _no_mass_power_sum_cache 22 | list _mass_elementary_symmetric_polynomial_cache 23 | list _mass_power_sum_cache 24 | 25 | cpdef double monoisotopic_mass(self) -------------------------------------------------------------------------------- /src/brainpy/_speedup.pyx: -------------------------------------------------------------------------------- 1 | # cython: embedsignature=True 2 | # cython: profile=False 3 | 4 | from cpython.list cimport PyList_GET_ITEM, PyList_GET_SIZE, PyList_Append 5 | from cpython.float cimport PyFloat_FromDouble, PyFloat_AsDouble 6 | from cpython.dict cimport PyDict_GetItem, PyDict_SetItem, PyDict_Keys, PyDict_Next 7 | from cpython.object cimport PyObject 8 | 9 | from libc.math cimport log, exp 10 | 11 | import operator 12 | import re 13 | 14 | mz_getter = operator.attrgetter("mz") 15 | 16 | from brainpy.mass_dict import nist_mass as _nist_mass 17 | from brainpy._c.isotopic_distribution cimport TheoreticalPeak as Peak 18 | 19 | 20 | nist_mass = _nist_mass 21 | PROTON = nist_mass["H+"][0][0] 22 | 23 | 24 | cdef double neutral_mass(double mz, int z, double charge_carrier=PROTON): 25 | return (mz * abs(z)) - (z * charge_carrier) 26 | 27 | 28 | cdef double mass_charge_ratio(double neutral_mass, int z, double charge_carrier=PROTON): 29 | return (neutral_mass + (z * charge_carrier)) / abs(z) 30 | 31 | 32 | cdef void _update_elementary_symmetric_polynomial(list power_sum, list elementary_symmetric_polynomial, size_t order): 33 | cdef: 34 | size_t begin, end, k, j 35 | double el 36 | int sign 37 | begin = PyList_GET_SIZE(elementary_symmetric_polynomial) 38 | end = PyList_GET_SIZE(power_sum) 39 | for k in range(begin, end): 40 | if k == 0: 41 | PyList_Append(elementary_symmetric_polynomial, 1.0) 42 | elif k > order: 43 | PyList_Append(elementary_symmetric_polynomial, 0.) 44 | else: 45 | el = 0. 46 | for j in range(1, k + 1): 47 | sign = 1 if (j % 2) == 1 else -1 48 | el += sign * PyFloat_AsDouble(PyList_GET_ITEM(power_sum, j)) * PyFloat_AsDouble(PyList_GET_ITEM(elementary_symmetric_polynomial, k - j)) 49 | el /= (k) 50 | PyList_Append(elementary_symmetric_polynomial, el) 51 | 52 | cdef void _update_power_sum(list ps_vec, list esp_vec, size_t order): 53 | cdef: 54 | size_t begin, end, k, j 55 | int sign 56 | double temp_ps 57 | begin = PyList_GET_SIZE(ps_vec) 58 | end = PyList_GET_SIZE(esp_vec) 59 | for k in range(begin, end): 60 | if k == 0: 61 | PyList_Append(ps_vec, 0.) 62 | continue 63 | temp_ps = 0. 64 | sign = -1 65 | for j in range(1, k): 66 | sign *= -1 67 | temp_ps += sign * PyFloat_AsDouble(PyList_GET_ITEM(esp_vec, j)) * PyFloat_AsDouble(PyList_GET_ITEM(ps_vec, k - j)) 68 | sign *= -1 69 | temp_ps += sign * PyFloat_AsDouble(PyList_GET_ITEM(esp_vec, k)) * k 70 | PyList_Append(ps_vec, temp_ps) 71 | 72 | cdef void newton(list power_sum, list elementary_symmetric_polynomial, int order): 73 | if PyList_GET_SIZE(power_sum) > PyList_GET_SIZE(elementary_symmetric_polynomial): 74 | _update_elementary_symmetric_polynomial(power_sum, elementary_symmetric_polynomial, order) 75 | elif PyList_GET_SIZE(power_sum) < PyList_GET_SIZE(elementary_symmetric_polynomial): 76 | _update_power_sum(power_sum, elementary_symmetric_polynomial, order) 77 | 78 | cdef list vietes(list coefficients): 79 | cdef: 80 | list elementary_symmetric_polynomial 81 | double tail, el 82 | size_t size, i 83 | int sign 84 | 85 | elementary_symmetric_polynomial = [] 86 | tail = float(coefficients[-1]) 87 | size = len(coefficients) 88 | 89 | for i in range(size): 90 | sign = 1 if (i % 2) == 0 else -1 91 | el = sign * coefficients[size - i - 1] / tail 92 | elementary_symmetric_polynomial.append(el) 93 | return elementary_symmetric_polynomial 94 | 95 | 96 | cdef class PolynomialParameters(object): 97 | cdef: 98 | public list elementary_symmetric_polynomial 99 | public list power_sum 100 | def __init__(self, elementary_symmetric_polynomial, power_sum): 101 | self.elementary_symmetric_polynomial = elementary_symmetric_polynomial 102 | self.power_sum = power_sum 103 | 104 | def __iter__(self): 105 | yield self.power_sum 106 | yield self.elementary_symmetric_polynomial 107 | 108 | 109 | cdef class PhiConstants(object): 110 | cdef: 111 | public int order 112 | public Element element 113 | public PolynomialParameters element_coefficients 114 | public PolynomialParameters mass_coefficients 115 | 116 | def __init__(self, order, element, element_coefficients, mass_coefficients): 117 | self.order = order 118 | self.element = element 119 | self.element_coefficients = element_coefficients 120 | self.mass_coefficients = mass_coefficients 121 | 122 | 123 | cdef class Isotope(object): 124 | """ 125 | Isotope represents an elenent with an integer number of neutrons specified. 126 | 127 | Attributes 128 | ---------- 129 | mass: float 130 | Measurable mass in Da 131 | abundance: float [0.0:1.0] 132 | The abundance of this isotope in nature. This number is between 0.0 and 1.0 133 | neutron_shift: int 134 | The number of neutrons different between this isotope and the "normal" form. May be 0 135 | if this represents that normal form. 136 | """ 137 | cdef: 138 | public double mass 139 | public double abundance 140 | public int neutron_shift 141 | public int neutrons 142 | 143 | def __init__(self, mass, abundance, neutron_shift, neutrons): 144 | self.mass = mass 145 | self.abundance = abundance 146 | self.neutron_shift = neutron_shift 147 | self.neutrons = neutrons 148 | 149 | def __repr__(self): 150 | return "Isotope(mass=%0.3f, abundance=%0.3f, neutron_shift=%d, neutrons=%d)" % ( 151 | self.mass, self.abundance, self.neutron_shift, self.neutrons) 152 | 153 | 154 | 155 | cdef int max_variants(dict composition): 156 | """Calculates the maximum number of isotopic variants that could be produced by a 157 | composition. 158 | 159 | Parameters 160 | ---------- 161 | composition : Mapping 162 | Any Mapping type where keys are element symbols and values are integers 163 | 164 | Returns 165 | ------- 166 | max_n_variants : int 167 | """ 168 | max_n_variants = 0 169 | 170 | for element, count in composition.items(): 171 | if element == "H+": 172 | continue 173 | try: 174 | max_n_variants += count * periodic_table[element].max_neutron_shift() 175 | except KeyError: 176 | pass 177 | 178 | return max_n_variants 179 | 180 | 181 | cdef double calculate_mass(dict composition, dict mass_data=None): 182 | cdef: 183 | double mass 184 | object match 185 | mass = 0.0 186 | if mass_data is None: 187 | mass_data = nist_mass 188 | for element in composition: 189 | try: 190 | mass += (composition[element] * mass_data[element][0][0]) 191 | except KeyError: 192 | match = re.search(r"(\S+)\[(\d+)\]", element) 193 | if match: 194 | element_ = match.group(1) 195 | isotope = int(match.group(2)) 196 | mass += composition[element] * mass_data[element_][isotope][0] 197 | else: 198 | raise 199 | 200 | return mass 201 | 202 | 203 | cdef str _make_isotope_string(str element, int isotope=0): 204 | if isotope == 0: 205 | return element 206 | else: 207 | return "%s[%d]" % (element, isotope) 208 | 209 | 210 | cdef tuple _get_isotope(str element_string): 211 | cdef: 212 | object match 213 | str element_ 214 | int isotope 215 | if "[" in element_string: 216 | match = re.search(r"(\S+)\[(\d+)\]", element_string) 217 | if match: 218 | element_ = match.group(1) 219 | isotope = int(match.group(2)) 220 | return element_, isotope 221 | else: 222 | return element_string, 0 223 | 224 | 225 | cpdef Element make_fixed_isotope_element(Element element, int neutrons): 226 | cdef: 227 | Isotope isotope 228 | Element el 229 | isotope = element.isotopes[neutrons - element.isotopes[0].neutrons] 230 | el = Element(element.symbol + ("[%d]" % neutrons), dict([ 231 | (0, Isotope(isotope.mass, abundance=1.0, neutron_shift=0, neutrons=neutrons)), 232 | ])) 233 | return el 234 | 235 | 236 | def _isotopes_of(element): 237 | freqs = dict() 238 | mono_neutrons = None 239 | element_data = nist_mass[element] 240 | for i, mass_freqs in element_data.items(): 241 | if i == 0: 242 | if isinstance(mass_freqs[0], int): 243 | mono_neutrons = mass_freqs[0] 244 | continue 245 | if mass_freqs[1] > 0: 246 | freqs[i] = mass_freqs 247 | if len(freqs) == 0: 248 | if mono_neutrons is not None and mono_neutrons in element_data: 249 | freqs = [ 250 | (0, Isotope(*element_data[mono_neutrons], neutron_shift=0, neutrons=mono_neutrons)) 251 | ] 252 | return dict(freqs) 253 | mono_neutrons = max(freqs.items(), key=lambda x: x[1][1])[0] 254 | freqs = list(sorted( 255 | [(k - mono_neutrons, Isotope(*v, neutron_shift=k - mono_neutrons, neutrons=k)) 256 | for k, v in freqs.items()], key=lambda x: x[0])) 257 | return dict(freqs) 258 | 259 | 260 | cdef class Element(object): 261 | 262 | def __init__(self, str symbol, dict isotopes=None): 263 | if isotopes is None: 264 | isotopes = _isotopes_of(symbol) 265 | self.symbol = symbol 266 | self.isotopes = isotopes 267 | min_shift = 1000 268 | max_shift = 0 269 | for shift in self.isotopes: 270 | if shift > max_shift: 271 | max_shift = shift 272 | if shift < min_shift: 273 | min_shift = shift 274 | self._min_neutron_shift = min_shift 275 | self._max_neutron_shift = max_shift 276 | self._no_mass_elementary_symmetric_polynomial_cache = None 277 | self._no_mass_power_sum_cache = None 278 | self._mass_elementary_symmetric_polynomial_cache = None 279 | self._mass_power_sum_cache = None 280 | try: 281 | self._monoisotopic_mass = self.isotopes[0].mass 282 | except: 283 | self._monoisotopic_mass = nist_mass[self.symbol][0][0] 284 | 285 | def __iter__(self): 286 | for key in sorted(self.isotopes.keys()): 287 | yield self.isotopes[key] 288 | 289 | def max_neutron_shift(self): 290 | return self._max_neutron_shift 291 | 292 | def min_neutron_shift(self): 293 | return self._min_neutron_shift 294 | 295 | cpdef double monoisotopic_mass(self): 296 | return self._monoisotopic_mass 297 | 298 | 299 | periodic_table = periodic_table = {k: Element(k) for k in nist_mass} 300 | 301 | 302 | cdef class IsotopicConstants(dict): 303 | cdef: 304 | public long _order 305 | 306 | def __init__(self, order): 307 | self._order = 0 308 | self.order = order 309 | 310 | property order: 311 | def __get__(self): 312 | return self._order 313 | 314 | def __set__(self, value): 315 | self._order = value 316 | self.update_coefficients() 317 | 318 | cdef PolynomialParameters coefficients(self, Element element, bint with_mass=False): 319 | cdef: 320 | int max_isotope_number, current_order 321 | list accumulator, isotope_keys 322 | Isotope isotope 323 | double coef 324 | size_t isotope_iter, i 325 | 326 | if with_mass: 327 | if element._mass_elementary_symmetric_polynomial_cache is not None: 328 | return PolynomialParameters( 329 | list(element._mass_elementary_symmetric_polynomial_cache), 330 | list(element._mass_power_sum_cache)) 331 | else: 332 | if element._no_mass_elementary_symmetric_polynomial_cache is not None: 333 | return PolynomialParameters( 334 | list(element._no_mass_elementary_symmetric_polynomial_cache), 335 | list(element._no_mass_power_sum_cache)) 336 | 337 | max_isotope_number = element._max_neutron_shift 338 | isotope_keys = sorted(element.isotopes, reverse=True) 339 | accumulator = [] 340 | for isotope_iter in range(PyList_GET_SIZE(isotope_keys)): 341 | isotope = PyDict_GetItem(element.isotopes, PyList_GET_ITEM(isotope_keys, isotope_iter)) 342 | current_order = max_isotope_number - isotope.neutron_shift 343 | if with_mass: 344 | coef = isotope.mass 345 | else: 346 | coef = 1. 347 | 348 | if current_order > len(accumulator): 349 | for i in range(len(accumulator), current_order): 350 | PyList_Append(accumulator, 0.) 351 | PyList_Append(accumulator, isotope.abundance * coef) 352 | elif current_order == len(accumulator): 353 | PyList_Append(accumulator, isotope.abundance * coef) 354 | else: 355 | raise Exception("The list of neutron shifts is not ordered.") 356 | 357 | elementary_symmetric_polynomial = vietes(accumulator) 358 | power_sum = [] 359 | newton(power_sum, elementary_symmetric_polynomial, len(accumulator) - 1) 360 | if with_mass: 361 | if element._mass_elementary_symmetric_polynomial_cache is None: 362 | element._mass_elementary_symmetric_polynomial_cache = list(elementary_symmetric_polynomial) 363 | element._mass_power_sum_cache = list(power_sum) 364 | else: 365 | if element._no_mass_elementary_symmetric_polynomial_cache is None: 366 | element._no_mass_elementary_symmetric_polynomial_cache = list(elementary_symmetric_polynomial) 367 | element._no_mass_power_sum_cache = list(power_sum) 368 | return PolynomialParameters(elementary_symmetric_polynomial, power_sum) 369 | 370 | cdef void add_element(self, str symbol): 371 | cdef: 372 | Element element 373 | int order 374 | int isotope 375 | str symbol_parsed 376 | PolynomialParameters element_parameters, mass_parameters 377 | 378 | if symbol in self: 379 | return 380 | try: 381 | element = periodic_table[symbol] 382 | except KeyError: 383 | symbol_parsed, isotope = _get_isotope(symbol) 384 | if isotope == 0: 385 | raise KeyError(symbol) 386 | element = make_fixed_isotope_element(periodic_table[symbol_parsed], isotope) 387 | order = element.max_neutron_shift() 388 | element_parameters = self.coefficients(element) 389 | mass_parameters = self.coefficients(element, True) 390 | self[symbol] = PhiConstants(order, element, element_parameters, mass_parameters) 391 | 392 | cdef void update_coefficients(self): 393 | cdef: 394 | str symbol 395 | PhiConstants phi_constants 396 | size_t i 397 | 398 | for symbol, phi_constants in self.items(): 399 | if self.order < phi_constants.order: 400 | continue 401 | 402 | for i in range(phi_constants.order, self.order + 1): 403 | phi_constants.element_coefficients.elementary_symmetric_polynomial.append(0.) 404 | phi_constants.mass_coefficients.elementary_symmetric_polynomial.append(0.) 405 | 406 | phi_constants.order = len(phi_constants.element_coefficients.elementary_symmetric_polynomial) 407 | newton(phi_constants.element_coefficients.power_sum, 408 | phi_constants.element_coefficients.elementary_symmetric_polynomial, 409 | phi_constants.order) 410 | newton(phi_constants.mass_coefficients.power_sum, 411 | phi_constants.mass_coefficients.elementary_symmetric_polynomial, 412 | phi_constants.order) 413 | 414 | cdef double nth_element_power_sum(self, str symbol, int order): 415 | cdef: 416 | PhiConstants constants 417 | constants = self[symbol] 418 | return constants.element_coefficients.power_sum[order] 419 | 420 | cdef double nth_modified_element_power_sum(self, str symbol, int order): 421 | cdef: 422 | PhiConstants constants 423 | constants = self[symbol] 424 | return constants.mass_coefficients.power_sum[order] 425 | 426 | 427 | cdef class IsotopicDistribution(object): 428 | cdef: 429 | public dict composition 430 | public IsotopicConstants _isotopic_constants 431 | public int _order 432 | public double average_mass 433 | public Peak monoisotopic_peak 434 | 435 | def __init__(self, composition, order=-1): 436 | self.composition = dict(composition) 437 | self._isotopic_constants = IsotopicConstants(order) 438 | self._order = 0 439 | self.order = order 440 | self.average_mass = 0. 441 | self.monoisotopic_peak = self._create_monoisotopic_peak() 442 | 443 | property order: 444 | def __get__(self): 445 | return self._order 446 | 447 | def __set__(self, value): 448 | max_variant_count = max_variants(self.composition) 449 | if value == -1: 450 | self._order = max_variant_count 451 | else: 452 | self._order = min(value, max_variant_count) 453 | self._update_isotopic_constants() 454 | 455 | cpdef _update_isotopic_constants(self): 456 | cdef: 457 | str element 458 | for element in self.composition: 459 | self._isotopic_constants.add_element(element) 460 | self._isotopic_constants.order = self._order 461 | 462 | cdef Peak _create_monoisotopic_peak(self): 463 | mass = calculate_mass(self.composition) 464 | intensity = 0. 465 | for element in self.composition: 466 | if element == "H+": 467 | continue 468 | # intensity += log(periodic_table[element].isotopes[0].abundance) 469 | intensity += log(self._isotopic_constants[element].element.isotopes[0].abundance) 470 | intensity = exp(intensity) 471 | return Peak(mass, intensity, 0) 472 | 473 | cdef double _phi_value(self, int order): 474 | cdef: 475 | double phi 476 | str element 477 | double count 478 | phi = 0. 479 | for element, count in self.composition.items(): 480 | if element == "H+": 481 | continue 482 | phi += self._isotopic_constants.nth_element_power_sum(element, order) * count 483 | return phi 484 | 485 | cdef double _modified_phi_value(self, str symbol, int order): 486 | cdef: 487 | double phi 488 | str element 489 | double count 490 | double coef 491 | Py_ssize_t pos 492 | PyObject* pk 493 | PyObject* pv 494 | 495 | phi = 0. 496 | pos = 0 497 | #for element, count in self.composition.items(): 498 | while PyDict_Next(self.composition, &pos, &pk, &pv): 499 | element = pk 500 | count = PyFloat_AsDouble(pv) 501 | if element == "H+": 502 | continue 503 | # Count is one lower for this symbol because an isotope is present 504 | # accounted for in the call to `nth_modified_element_power_sum` at 505 | # the end? 506 | coef = (count if element != symbol else count - 1) 507 | phi += self._isotopic_constants.nth_element_power_sum(element, order) * coef 508 | 509 | phi += self._isotopic_constants.nth_modified_element_power_sum(symbol, order) 510 | return phi 511 | 512 | cpdef list phi_values(self): 513 | cdef: 514 | list power_sum 515 | size_t i 516 | power_sum = [0.] 517 | for i in range(1, self.order + 1): 518 | power_sum.append(self._phi_value(i)) 519 | return power_sum 520 | 521 | cpdef list modified_phi_values(self, symbol): 522 | cdef: 523 | list power_sum 524 | size_t i 525 | power_sum = [0.] 526 | for i in range(1, self.order + 1): 527 | power_sum.append(self._modified_phi_value(symbol, i)) 528 | return power_sum 529 | 530 | cpdef list probability(self): 531 | cdef: 532 | list phi_values 533 | int max_variant_count 534 | list probability_vector 535 | size_t i 536 | int sign 537 | 538 | phi_values = self.phi_values() 539 | max_variant_count = max_variants(self.composition) 540 | probability_vector = [] 541 | newton(phi_values, probability_vector, max_variant_count) 542 | 543 | for i in range(0, len(probability_vector)): 544 | # The sign of each term in the probability vector (populated by 545 | # Newton's Identities by solving for the Elementary Symmetric Polynomial 546 | # given the Power Sums) alternates in the same order as `sign`. 547 | # This ensures that the probability vector is strictly positive. 548 | sign = 1 if i % 2 == 0 else -1 549 | # q(j) = q(0) * e(j) * (-1)^j 550 | # intensity of the jth peak is |probability[j]| * the intensity of monoisotopic peak 551 | probability_vector[i] *= self.monoisotopic_peak.intensity * sign 552 | 553 | return probability_vector 554 | 555 | cpdef list center_mass(self, list probability_vector): 556 | cdef: 557 | list mass_vector 558 | list composition_elements 559 | int max_variant_count, sign 560 | dict ele_sym_poly_map 561 | str element 562 | list ele_sym_poly 563 | list power_sum 564 | size_t i, j 565 | Py_ssize_t k 566 | Element element_obj 567 | PhiConstants phi_obj 568 | double center, temp 569 | double _element_count, polynomial_term, _monoisotopic_mass 570 | double base_intensity 571 | PyObject* pk 572 | PyObject* pv 573 | 574 | mass_vector = [] 575 | max_variant_count = max_variants(self.composition) 576 | 577 | base_intensity = self.monoisotopic_peak.intensity 578 | ele_sym_poly_map = dict() 579 | composition_elements = PyDict_Keys(self.composition) 580 | 581 | for j in range(PyList_GET_SIZE(composition_elements)): 582 | element = PyList_GET_ITEM(composition_elements, j) 583 | if element == "H+": 584 | continue 585 | power_sum = self.modified_phi_values(element) 586 | ele_sym_poly = [] 587 | newton(power_sum, ele_sym_poly, max_variant_count) 588 | ele_sym_poly_map[element] = ele_sym_poly 589 | for i in range(self._order + 1): 590 | sign = 1 if i % 2 == 0 else -1 591 | center = 0.0 592 | k = 0 593 | while(PyDict_Next(ele_sym_poly_map, &k, &pk, &pv)): 594 | 595 | #for element, ele_sym_poly in ele_sym_poly_map.items(): 596 | element = pk 597 | ele_sym_poly = pv 598 | 599 | _element_count = PyFloat_AsDouble(PyDict_GetItem(self.composition, element)) 600 | 601 | polynomial_term = PyFloat_AsDouble(PyList_GET_ITEM(ele_sym_poly, i)) 602 | 603 | phi_obj = PyDict_GetItem(self._isotopic_constants, element) 604 | element_obj = phi_obj.element 605 | # element_obj = (PyDict_GetItem(periodic_table, element)) 606 | _monoisotopic_mass = element_obj._monoisotopic_mass 607 | 608 | temp = _element_count 609 | temp *= sign * polynomial_term 610 | temp *= base_intensity * _monoisotopic_mass 611 | center += temp 612 | 613 | mass_vector.append((center / probability_vector[i]) if probability_vector[i] > 0 else 0) 614 | return mass_vector 615 | 616 | def aggregated_isotopic_variants(self, int charge=0, double charge_carrier=PROTON): 617 | """ 618 | Compute the m/z (or neutral mass when `charge` == 0) for each 619 | aggregated isotopic peak and their intensity relative to 620 | the monoisotopic peak. 621 | """ 622 | cdef: 623 | list probability_vector 624 | list center_mass_vector 625 | list peak_set 626 | double average_mass, adjusted_mz 627 | double total 628 | size_t i 629 | Peak peak 630 | double center_mass_i, intensity_i 631 | probability_vector = self.probability() 632 | center_mass_vector = self.center_mass(probability_vector) 633 | 634 | peak_set = [] 635 | average_mass = 0. 636 | total = sum(probability_vector) 637 | 638 | for i in range(self.order + 1): 639 | center_mass_i = PyFloat_AsDouble(PyList_GET_ITEM(center_mass_vector, i)) 640 | if charge != 0: 641 | adjusted_mz = mass_charge_ratio(center_mass_i, charge, charge_carrier) 642 | else: 643 | adjusted_mz = center_mass_i 644 | 645 | intensity_i = PyFloat_AsDouble(PyList_GET_ITEM(probability_vector, i)) 646 | 647 | peak = Peak(adjusted_mz, intensity_i / total, charge) 648 | if peak.intensity < 0: 649 | continue 650 | peak_set.append(peak) 651 | average_mass += adjusted_mz * intensity_i 652 | 653 | average_mass /= total 654 | self.average_mass = average_mass 655 | peak_set.sort(key=mz_getter) 656 | return tuple(peak_set) 657 | -------------------------------------------------------------------------------- /src/brainpy/brainpy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | 4 | import operator 5 | 6 | from collections import OrderedDict 7 | from math import exp, log, sqrt, isinf 8 | from sys import float_info 9 | 10 | from brainpy.mass_dict import nist_mass 11 | from brainpy.composition import ( 12 | PyComposition, 13 | parse_formula, 14 | calculate_mass, 15 | _make_isotope_string, 16 | _get_isotope) 17 | 18 | mz_getter = operator.attrgetter("mz") 19 | PROTON = nist_mass["H+"][0][0] 20 | MACHINE_EPSILON = float_info.epsilon 21 | ZERO = 0. 22 | ONE = 1.0 23 | 24 | 25 | def neutral_mass(mz, z, charge_carrier=PROTON): 26 | return (mz * abs(z)) - (z * charge_carrier) 27 | 28 | 29 | def mass_charge_ratio(neutral_mass, z, charge_carrier=PROTON): 30 | return (neutral_mass + (z * charge_carrier)) / abs(z) 31 | 32 | 33 | def give_repr(cls): # pragma: no cover 34 | r"""Patch a class to give it a generic __repr__ method 35 | that works by inspecting the instance dictionary. 36 | 37 | Parameters 38 | ---------- 39 | cls: type 40 | The class to add a generic __repr__ to. 41 | 42 | Returns 43 | ------- 44 | cls: type 45 | The passed class is returned 46 | """ 47 | def reprer(self): 48 | attribs = ', '.join(["%s=%r" % (k, v) for k, v in self.__dict__.items() if not k.startswith("_")]) 49 | wrap = "{self.__class__.__name__}({attribs})".format(self=self, attribs=attribs) 50 | return wrap 51 | cls.__repr__ = reprer 52 | return cls 53 | 54 | 55 | @give_repr 56 | class PolynomialParameters(object): 57 | def __init__(self, elementary_symmetric_polynomial, power_sum): 58 | self.elementary_symmetric_polynomial = elementary_symmetric_polynomial 59 | self.power_sum = power_sum 60 | 61 | def __iter__(self): 62 | yield self.power_sum 63 | yield self.elementary_symmetric_polynomial 64 | 65 | 66 | @give_repr 67 | class PhiConstants(object): 68 | def __init__(self, order, element, element_coefficients, mass_coefficients): 69 | self.order = order 70 | self.element = element 71 | self.element_coefficients = element_coefficients 72 | self.mass_coefficients = mass_coefficients 73 | 74 | 75 | def newton(power_sum, elementary_symmetric_polynomial, order): 76 | r""" 77 | Given two lists of values, the first list being the `power sum`s of a 78 | polynomial, and the second being expressions of the roots of the 79 | polynomial as found by Viete's Formula, use information from the longer list to 80 | fill out the shorter list using Newton's Identities. 81 | 82 | .. note:: 83 | Updates are done **in place** 84 | 85 | Parameters 86 | ---------- 87 | power_sum: list of float 88 | elementary_symmetric_polynomial: list of float 89 | order: int 90 | The number of terms to expand to when updating `elementary_symmetric_polynomial` 91 | 92 | See Also 93 | -------- 94 | https://en.wikipedia.org/wiki/Newton%27s_identities[https://en.wikipedia.org/wiki/Newton%27s_identities] 95 | """ 96 | if len(power_sum) > len(elementary_symmetric_polynomial): 97 | _update_elementary_symmetric_polynomial(power_sum, elementary_symmetric_polynomial, order) 98 | elif len(power_sum) < len(elementary_symmetric_polynomial): 99 | _update_power_sum(power_sum, elementary_symmetric_polynomial, order) 100 | 101 | 102 | def _update_elementary_symmetric_polynomial(power_sum, elementary_symmetric_polynomial, order): 103 | begin = len(elementary_symmetric_polynomial) 104 | end = len(power_sum) 105 | for k in range(begin, end): 106 | if k == 0: 107 | elementary_symmetric_polynomial.append(1.0) 108 | elif k > order: 109 | elementary_symmetric_polynomial.append(0.) 110 | else: 111 | el = 0. 112 | for j in range(1, k + 1): 113 | sign = 1 if (j % 2) == 1 else -1 114 | el += sign * power_sum[j] * elementary_symmetric_polynomial[k - j] 115 | el /= float(k) 116 | elementary_symmetric_polynomial.append(el) 117 | 118 | 119 | def _update_power_sum(ps_vec, esp_vec, order): 120 | begin = len(ps_vec) 121 | end = len(esp_vec) 122 | for k in range(begin, end): 123 | if k == 0: 124 | ps_vec.append(0.) 125 | continue 126 | temp_ps = 0. 127 | sign = -1 128 | for j in range(1, k): 129 | sign *= -1 130 | temp_ps += sign * esp_vec[j] * ps_vec[k - j] 131 | sign *= -1 132 | temp_ps += sign * esp_vec[k] * k 133 | ps_vec.append(temp_ps) 134 | 135 | 136 | def vietes(coefficients): 137 | r""" 138 | Given the coefficients of a polynomial of a single variable, 139 | compute an elementary symmetric polynomial of the roots of the 140 | input polynomial by Viete's Formula: 141 | 142 | .. math:: 143 | \sum_{1\le i_1 0: 254 | freqs[i] = mass_freqs 255 | if len(freqs) == 0: 256 | if mono_neutrons is not None and mono_neutrons in element_data: 257 | freqs = [ 258 | (0, Isotope(*element_data[mono_neutrons], neutron_shift=0, neutrons=mono_neutrons)) 259 | ] 260 | return OrderedDict(freqs) 261 | mono_neutrons = max(freqs.items(), key=lambda x: x[1][1])[0] 262 | freqs = OrderedDict(sorted(((k - mono_neutrons, Isotope(*v, neutron_shift=k - mono_neutrons, neutrons=k)) 263 | for k, v in freqs.items()), key=lambda x: x[0])) 264 | return freqs 265 | 266 | 267 | periodic_table = {k: Element(k) for k in nist_mass} 268 | # periodic_table = {k: e for k, e in periodic_table.items() if e.max_neutron_shift() != 0 and e.min_neutron_shift() >= 0} 269 | periodic_table["H+"] = Element("H+") 270 | 271 | 272 | class IsotopicConstants(dict): 273 | def __init__(self, order): 274 | self._order = 0 275 | self.order = order 276 | 277 | @property 278 | def order(self): 279 | return self._order 280 | 281 | @order.setter 282 | def order(self, value): 283 | self._order = value 284 | self.update_coefficients() 285 | 286 | def coefficients(self, element, with_mass=False): 287 | max_isotope_number = element.max_neutron_shift() 288 | accumulator = [] 289 | for isotope in reversed(list(element)): 290 | current_order = max_isotope_number - isotope.neutron_shift 291 | if with_mass: 292 | coef = isotope.mass 293 | else: 294 | coef = 1. 295 | 296 | if current_order > len(accumulator): 297 | for i in range(len(accumulator), current_order): 298 | accumulator.append(0.) 299 | accumulator.append(isotope.abundance * coef) 300 | elif current_order == len(accumulator): 301 | accumulator.append(isotope.abundance * coef) 302 | else: 303 | raise Exception("The list of neutron shifts is not ordered.") 304 | 305 | elementary_symmetric_polynomial = vietes(accumulator) 306 | power_sum = [] 307 | newton(power_sum, elementary_symmetric_polynomial, len(accumulator) - 1) 308 | return PolynomialParameters(elementary_symmetric_polynomial, power_sum) 309 | 310 | def add_element(self, symbol): 311 | if symbol in self: 312 | return 313 | if symbol in periodic_table: 314 | element = periodic_table[symbol] 315 | else: 316 | symbol_parsed, isotope = _get_isotope(symbol) 317 | if isotope == 0: 318 | raise KeyError(symbol) 319 | element = make_fixed_isotope_element(periodic_table[symbol_parsed], isotope) 320 | order = element.max_neutron_shift() 321 | element_parameters = self.coefficients(element) 322 | mass_parameters = self.coefficients(element, True) 323 | self[symbol] = PhiConstants(order, element, element_parameters, mass_parameters) 324 | 325 | def update_coefficients(self): 326 | for symbol, phi_constants in self.items(): 327 | if self.order < phi_constants.order: 328 | continue 329 | 330 | for i in range(phi_constants.order, self.order + 1): 331 | phi_constants.element_coefficients.elementary_symmetric_polynomial.append(0.) 332 | phi_constants.mass_coefficients.elementary_symmetric_polynomial.append(0.) 333 | 334 | phi_constants.order = len(phi_constants.element_coefficients.elementary_symmetric_polynomial) 335 | newton(*phi_constants.element_coefficients, order=phi_constants.order) 336 | newton(*phi_constants.mass_coefficients, order=phi_constants.order) 337 | 338 | def nth_element_power_sum(self, symbol, order): 339 | constants = self[symbol] 340 | return constants.element_coefficients.power_sum[order] 341 | 342 | def nth_modified_element_power_sum(self, symbol, order): 343 | constants = self[symbol] 344 | return constants.mass_coefficients.power_sum[order] 345 | 346 | 347 | def max_variants(composition): 348 | """Calculates the maximum number of isotopic variants that could be produced by a 349 | composition. 350 | 351 | Parameters 352 | ---------- 353 | composition : Mapping 354 | Any Mapping type where keys are element symbols and values are integers 355 | 356 | Returns 357 | ------- 358 | max_n_variants : int 359 | """ 360 | max_n_variants = 0 361 | 362 | for element, count in composition.items(): 363 | if element == "H+": 364 | continue 365 | try: 366 | max_n_variants += count * periodic_table[element].max_neutron_shift() 367 | except KeyError: 368 | pass 369 | 370 | return max_n_variants 371 | 372 | 373 | def max_variants_approx(mass, lambda_factor=1800.0, maxiter=255, threshold=0.9999): 374 | """Approximate the maximum number of isotopic peaks to include in an isotopic distribution 375 | approximation for biomolecules using the Poisson distribution, using the method described 376 | in Bellew et al [1]. 377 | 378 | This algorithm adds peaks to the approximated isotopic pattern until `threshold`% signal 379 | is generated. 380 | 381 | Parameters 382 | ----------- 383 | mass : double 384 | The mass generate th approximate pattern for. 385 | lambda_factor : double 386 | The initial Poisson parameter. The default value is taken from the original 387 | reference [1] 388 | maxiter : int 389 | The maximum number of iterations to run for, also the maximum number of peaks 390 | that can be added. This approximation becomes more and more inaccurate the 391 | larger the value gets. 392 | threshold : double 393 | The percentage of the total signal to collect until terminating. 394 | 395 | Returns 396 | ------- 397 | n_peaks : int 398 | The number of isotopic peaks to generate, or exceeded the `maxiter` if 0 399 | 400 | References 401 | ---------- 402 | [1] Bellew, M., Coram, M., Fitzgibbon, M., Igra, M., Randolph, T., Wang, P., May, D., Eng, J., Fang, R., Lin, C., Chen, J., 403 | Goodlett, D., Whiteaker, J., Paulovich, A., & Mcintosh, M. (2006). A suite of algorithms for the comprehensive analysis 404 | of complex protein mixtures using high-resolution LC-MS. 22(15), 1902–1909. https://doi.org/10.1093/bioinformatics/btl276 405 | """ 406 | lmbda = mass / lambda_factor 407 | p_i = 1.0 408 | factorial_acc = 1.0 409 | acc = 1.0 410 | threshold = 1.0 - threshold 411 | for i in range(1, maxiter): 412 | p_i *= lmbda 413 | factorial_acc *= i 414 | cur_intensity = p_i / factorial_acc 415 | if isinf(cur_intensity): 416 | return i 417 | acc += cur_intensity 418 | if cur_intensity / acc < threshold: 419 | return i 420 | return 0 421 | 422 | 423 | 424 | 425 | @give_repr 426 | class Peak(object): 427 | """ 428 | Represent a single theoretical peak centroid. 429 | 430 | Peaks are comparable, hashable, and can be copied by calling 431 | :meth:`clone` 432 | 433 | Attributes 434 | ---------- 435 | charge : int 436 | The charge state of the peak 437 | intensity : float 438 | The height of the peak. Peaks created as part of a 439 | theoretical isotopic cluster will be have an intensity 440 | between 0 and 1. 441 | mz : float 442 | The mass-to-charge ratio of the peak 443 | """ 444 | def __init__(self, mz, intensity, charge): 445 | self.mz = mz 446 | self.intensity = intensity 447 | self.charge = charge 448 | 449 | def __eq__(self, other): # pragma: no cover 450 | equal = all( 451 | abs(self.mz - other.mz) < 1e-10, 452 | abs(self.intensity - other.intensity) < 1e-10, 453 | self.charge == other.charge) 454 | return equal 455 | 456 | def __ne__(self, other): # pragma: no cover 457 | return not (self == other) 458 | 459 | def __hash__(self): # pragma: no cover 460 | return hash(self.mz) 461 | 462 | def clone(self): # pragma: no cover 463 | return self.__class__(self.mz, self.intensity, self.charge) 464 | 465 | 466 | @give_repr 467 | class IsotopicDistribution(object): 468 | """ 469 | Constructs a theoretical isotopic distribution for a given composition 470 | out to a given number of peaks. 471 | 472 | Attributes 473 | ---------- 474 | average_mass : float 475 | The average (weighted) mass of the resulting isotopic cluster 476 | composition : dict 477 | The composition to create the isotopic cluster for 478 | order : int 479 | The number of peaks to produce and the number of terms in the 480 | generating polynomial expression. 481 | """ 482 | def __init__(self, composition, order=-1): 483 | self.composition = composition 484 | self._isotopic_constants = IsotopicConstants(order) 485 | self._order = 0 486 | self.order = order 487 | self.average_mass = 0. 488 | self.monoisotopic_peak = self._create_monoisotopic_peak() 489 | 490 | @property 491 | def order(self): 492 | return self._order 493 | 494 | @order.setter 495 | def order(self, value): 496 | max_variant_count = max_variants(self.composition) 497 | if value == -1: 498 | self._order = max_variant_count 499 | else: 500 | self._order = min(value, max_variant_count) 501 | self._update_isotopic_constants() 502 | 503 | def _update_isotopic_constants(self): 504 | for element in self.composition: 505 | self._isotopic_constants.add_element(element) 506 | self._isotopic_constants.order = self._order 507 | 508 | def _create_monoisotopic_peak(self): 509 | mass = calculate_mass(self.composition) 510 | intensity = 0. 511 | for element in self.composition: 512 | if element == "H+": 513 | continue 514 | # intensity += log(periodic_table[element].isotopes[0].abundance) 515 | intensity += log(self._isotopic_constants[element].element.isotopes[0].abundance) 516 | intensity = exp(intensity) 517 | return Peak(mass, intensity, 0) 518 | 519 | def _phi_value(self, order): 520 | phi = 0. 521 | for element, count in self.composition.items(): 522 | if element == "H+": 523 | continue 524 | phi += self._isotopic_constants.nth_element_power_sum(element, order) * count 525 | return phi 526 | 527 | def _modified_phi_value(self, symbol, order): 528 | phi = 0. 529 | for element, count in self.composition.items(): 530 | if element == "H+": 531 | continue 532 | # Count is one lower for this symbol because an isotope is present 533 | # accounted for in the call to `nth_modified_element_power_sum` at 534 | # the end? 535 | coef = count if element != symbol else count - 1 536 | phi += self._isotopic_constants.nth_element_power_sum(element, order) * coef 537 | 538 | phi += self._isotopic_constants.nth_modified_element_power_sum(symbol, order) 539 | return phi 540 | 541 | def phi_values(self): 542 | power_sum = [0.] 543 | for i in range(1, self.order + 1): 544 | power_sum.append(self._phi_value(i)) 545 | return power_sum 546 | 547 | def modified_phi_values(self, symbol): 548 | power_sum = [0.] 549 | for i in range(1, self.order + 1): 550 | power_sum.append(self._modified_phi_value(symbol, i)) 551 | return power_sum 552 | 553 | def probability(self): 554 | phi_values = self.phi_values() 555 | max_variant_count = max_variants(self.composition) 556 | probability_vector = [] 557 | newton(phi_values, probability_vector, max_variant_count) 558 | 559 | for i in range(0, len(probability_vector)): 560 | # The sign of each term in the probability vector (populated by 561 | # Newton's Identities by solving for the Elementary Symmetric Polynomial 562 | # given the Power Sums) alternates in the same order as `sign`. 563 | # This ensures that the probability vector is strictly positive. 564 | sign = 1 if i % 2 == 0 else -1 565 | # q(j) = q(0) * e(j) * (-1)^j 566 | # intensity of the jth peak is |probability[j]| * the intensity of monoisotopic peak 567 | probability_vector[i] *= self.monoisotopic_peak.intensity * sign 568 | 569 | return probability_vector 570 | 571 | def center_mass(self, probability_vector): 572 | mass_vector = [] 573 | max_variant_count = max_variants(self.composition) 574 | 575 | ele_sym_poly_map = dict() 576 | for element in self.composition: 577 | if element == "H+": 578 | continue 579 | power_sum = self.modified_phi_values(element) 580 | ele_sym_poly = [] 581 | newton(power_sum, ele_sym_poly, max_variant_count) 582 | ele_sym_poly_map[element] = ele_sym_poly 583 | for i in range(self.order + 1): 584 | sign = 1 if i % 2 == 0 else -1 585 | center = 0.0 586 | for element, ele_sym_poly in ele_sym_poly_map.items(): 587 | center += self.composition[element] * sign * ele_sym_poly[i] *\ 588 | self.monoisotopic_peak.intensity * self._isotopic_constants[element].element.monoisotopic_mass() 589 | # self.monoisotopic_peak.intensity * periodic_table[element].monoisotopic_mass() 590 | mass_vector.append((center / probability_vector[i]) if probability_vector[i] > 0 else 0) 591 | return mass_vector 592 | 593 | def aggregated_isotopic_variants(self, charge=0, charge_carrier=PROTON): 594 | """ 595 | Compute the m/z (or neutral mass when `charge` == 0) for each 596 | aggregated isotopic peak and their intensity relative to 597 | the monoisotopic peak. 598 | 599 | Parameters 600 | ---------- 601 | charge: int 602 | The charge state of the resulting theoretical isotopic cluster 603 | charge_carrier: float 604 | The mass added for each degree of charge 605 | 606 | Returns 607 | ------- 608 | theoretical_isotopic_distribution: list 609 | A list of :class:`Peak` objects whose intensities are proportional to 610 | each other to reflect relative peak heights. 611 | 612 | """ 613 | probability_vector = self.probability() 614 | center_mass_vector = self.center_mass(probability_vector) 615 | 616 | peak_set = [] 617 | average_mass = 0. 618 | total = sum(probability_vector) 619 | 620 | for i in range(self.order + 1): 621 | if charge != 0: 622 | adjusted_mz = mass_charge_ratio(center_mass_vector[i], charge, charge_carrier) 623 | else: 624 | adjusted_mz = center_mass_vector[i] 625 | if adjusted_mz < 1: 626 | continue 627 | peak = Peak(adjusted_mz, probability_vector[i] / total, charge) 628 | if peak.intensity < 0: 629 | continue 630 | peak_set.append(peak) 631 | average_mass += adjusted_mz * probability_vector[i] 632 | 633 | average_mass /= total 634 | self.average_mass = average_mass 635 | peak_set.sort(key=mz_getter) 636 | return tuple(peak_set) 637 | 638 | 639 | def isotopic_variants(composition, npeaks=None, charge=0, charge_carrier=PROTON): 640 | """ 641 | Compute a peak list representing the theoretical isotopic cluster for `composition`. 642 | 643 | Parameters 644 | ---------- 645 | composition : Mapping 646 | Any Mapping type where keys are element symbols and values are integers 647 | npeaks: int 648 | The number of peaks to include in the isotopic cluster, starting from the monoisotopic peak. 649 | If given a number below 1 or above the maximum number of isotopic variants, the maximum will 650 | be used. If `None`, a "reasonable" value is chosen by `int(sqrt(max_variants(composition)))`. 651 | charge: int 652 | The charge state of the isotopic cluster to produce. Defaults to 0, theoretical neutral mass. 653 | 654 | Returns 655 | ------- 656 | list of Peaks 657 | 658 | See Also 659 | -------- 660 | :class:`IsotopicDistribution` 661 | 662 | """ 663 | if npeaks is None: 664 | max_n_variants = max_variants(composition) 665 | npeaks = int(sqrt(max_n_variants) - 2) 666 | npeaks = max(npeaks, 3) 667 | else: 668 | # Monoisotopic Peak is not included 669 | npeaks -= 1 670 | return IsotopicDistribution(composition, npeaks).aggregated_isotopic_variants( 671 | charge, charge_carrier=charge_carrier) 672 | 673 | 674 | try: 675 | _has_c = True 676 | _IsotopicDistribution = IsotopicDistribution 677 | from ._speedup import IsotopicDistribution 678 | from ._c.isotopic_distribution import pyisotopic_variants as isotopic_variants, py_max_variants_approx as max_variants_approx 679 | 680 | except ImportError: 681 | _has_c = False 682 | -------------------------------------------------------------------------------- /src/brainpy/composition.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from collections import Counter 4 | 5 | from .mass_dict import nist_mass 6 | 7 | _isotope_string = r'^([A-Z][a-z+]*)(?:\[(\d+)\])?$' 8 | _atom = r'([A-Z][a-z+]*)(?:\[(\d+)\])?([+-]?\d+)?' 9 | _formula = r'^({})*$'.format(_atom) 10 | atom_pattern = re.compile(_atom) 11 | formula_pattern = re.compile(_formula) 12 | 13 | 14 | def calculate_mass(composition, mass_data=None): 15 | """Calculates the monoisotopic mass of a composition 16 | 17 | Parameters 18 | ---------- 19 | composition : Mapping 20 | Any Mapping type where keys are element symbols and values are integers 21 | mass_data : dict, optional 22 | A dict with the masses of the chemical elements (the default 23 | value is :py:data:`nist_mass`). 24 | 25 | Returns 26 | ------- 27 | mass : float 28 | """ 29 | mass = 0.0 30 | if mass_data is None: 31 | mass_data = nist_mass 32 | for element in composition: 33 | try: 34 | mass += (composition[element] * mass_data[element][0][0]) 35 | except KeyError: 36 | match = re.search(r"(\S+)\[(\d+)\]", element) 37 | if match: 38 | element_ = match.group(1) 39 | isotope = int(match.group(2)) 40 | mass += composition[element] * mass_data[element_][isotope][0] 41 | else: 42 | raise 43 | return mass 44 | 45 | 46 | def _make_isotope_string(element, isotope=0): 47 | if isotope == 0: 48 | return element 49 | else: 50 | return "%s[%d]" % (element, isotope) 51 | 52 | 53 | def _get_isotope(element_string): 54 | if "[" in element_string: 55 | match = re.search(r"(\S+)\[(\d+)\]", element_string) 56 | if match: 57 | element_ = match.group(1) 58 | isotope = int(match.group(2)) 59 | return element_, isotope 60 | else: 61 | return element_string, 0 62 | 63 | 64 | class PyComposition(Counter): 65 | """A mapping representing a chemical composition. 66 | 67 | Implements arithmetic operations, +/- is defined 68 | between a :class:`PyComposition` and a :class:`Mapping`-like 69 | object, and * is defined between a :class:`PyComposition` and 70 | an integer. 71 | """ 72 | def __init__(self, base=None, **kwargs): 73 | if base is not None: 74 | self.update(base) 75 | else: 76 | if kwargs: 77 | self.update(kwargs) 78 | 79 | def __missing__(self, key): 80 | return 0 81 | 82 | def __mul__(self, i): 83 | inst = self.copy() 84 | for key, value in self.items(): 85 | inst[key] = value * i 86 | return inst 87 | 88 | def __imul__(self, i): 89 | for key, value in list(self.items()): 90 | self[key] = value * i 91 | return self 92 | 93 | def mass(self, mass_data=None): 94 | """Calculate the monoisotopic mass of this chemical composition 95 | 96 | Returns 97 | ------- 98 | float 99 | """ 100 | return calculate_mass(self, mass_data) 101 | 102 | 103 | def parse_formula(formula): 104 | """Parse a chemical formula and construct a :class:`PyComposition` object 105 | 106 | The formula must be made up of zero or more pieces following the pattern 107 | ``([A-Z][a-z]*)(\[\d+\])?(\d+)``. 108 | 109 | Parameters 110 | ---------- 111 | formula : :class:`str` 112 | 113 | Returns 114 | ------- 115 | :class:`PyComposition` 116 | 117 | Examples 118 | -------- 119 | >>>parse_formula("H2O1") # The 1 following the O must be present 120 | PyComposition({"H": 2, "O": 1}) 121 | >>>parse_formula("C34H53O15N7").mass() 122 | 799.35996402671 123 | >>>parse_formula("C7H15C[13]1O6N[15]1") 124 | PyComposition({"C": 7, "H": 15, "C[13]": 1, "O": 6, "N[15]": 1}) 125 | >>>parse_formula("C7H15C[13]1O6N[15]1").mass() 126 | 223.09032693441 127 | 128 | Raises 129 | ------ 130 | ValueError 131 | If the formula doesn't match the expected pattern 132 | """ 133 | if not formula_pattern.match(formula): 134 | raise ValueError("%r does not look like a formula" % (formula,)) 135 | composition = PyComposition() 136 | for elem, isotope, number in atom_pattern.findall(formula): 137 | composition[_make_isotope_string(elem, int(isotope) if isotope else 0)] += int(number) 138 | return composition 139 | 140 | 141 | try: 142 | _has_c = True 143 | _parse_formula = parse_formula 144 | _PyComposition = PyComposition 145 | from ._c.composition import parse_formula, PyComposition 146 | except ImportError as e: 147 | print(e) 148 | _has_c = False 149 | 150 | 151 | SimpleComposition = PyComposition 152 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mobiusklein/brainpy/f2ee5be540019ac833bbd1767a02fa182016d30a/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_composition.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from brainpy.composition import _parse_formula as parse_formula, calculate_mass 4 | from brainpy import _has_c 5 | 6 | if _has_c: 7 | from brainpy._c.composition import parse_formula as cparse_formula 8 | 9 | try: 10 | import faulthandler 11 | faulthandler.enable() 12 | except ImportError: 13 | pass 14 | 15 | 16 | class CompositionTest(unittest.TestCase): 17 | def test_parse(self): 18 | formula = "C6H12O6" 19 | composition = parse_formula(formula) 20 | self.assertEqual(composition, {"C": 6, "H": 12, "O": 6}) 21 | self.assertEqual(composition * 2, {"C": 6 * 2, "H": 12 * 2, "O": 6 * 2}) 22 | if _has_c: 23 | composition = cparse_formula(formula) 24 | self.assertEqual(composition, {"C": 6, "H": 12, "O": 6}) 25 | self.assertEqual(composition * 2, {"C": 6 * 2, "H": 12 * 2, "O": 6 * 2}) 26 | 27 | def test_isotope_parse(self): 28 | for formula in ["O1H1H1H[2]1", "O1H1H[2]1H1"]: 29 | composition = parse_formula(formula) 30 | self.assertEqual(composition, {"O": 1, "H": 2, "H[2]": 1}, msg="%r != %r" % ( 31 | composition, {"O": 1, "H": 2, "H[2]": 1})) 32 | self.assertAlmostEqual(composition.mass(), 20.0246, 3, msg="%r.mass() (%r) != %r" % ( 33 | composition, composition.mass(), 20.0246)) 34 | self.assertEqual(composition['H'], 2) 35 | if _has_c: 36 | composition = cparse_formula(formula) 37 | self.assertEqual(composition, {"O": 1, "H": 2, "H[2]": 1}, msg="%r != %r" % ( 38 | composition, {"O": 1, "H": 2, "H[2]": 1})) 39 | self.assertAlmostEqual(composition.mass(), 20.0246, 3, msg="%r.mass() (%r) != %r" % ( 40 | composition, composition.mass(), 20.0246)) 41 | self.assertEqual(composition['H'], 2) 42 | 43 | def test_mass(self): 44 | formula = "C6H12O6" 45 | composition = parse_formula(formula) 46 | self.assertEqual(composition['C'], 6) 47 | self.assertEqual(composition['H'], 12) 48 | self.assertEqual(composition['O'], 6) 49 | self.assertAlmostEqual(composition.mass(), calculate_mass({"C": 6, "H": 12, "O": 6}), 50 | msg="%s did not have the right mass (%f)" % (composition, composition.mass())) 51 | if _has_c: 52 | ccomposition = cparse_formula(formula) 53 | self.assertEqual(ccomposition['C'], 6) 54 | self.assertEqual(ccomposition['H'], 12) 55 | self.assertEqual(ccomposition['O'], 6) 56 | self.assertAlmostEqual(ccomposition.mass(), calculate_mass({"C": 6, "H": 12, "O": 6}), 57 | msg="%s did not have the right mass (%f)" % (ccomposition, ccomposition.mass())) 58 | 59 | 60 | if __name__ == '__main__': 61 | unittest.main() 62 | -------------------------------------------------------------------------------- /tests/test_isotopic_distribution.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from brainpy import IsotopicDistribution, isotopic_variants, calculate_mass, neutral_mass, _has_c, Peak 4 | 5 | if _has_c: 6 | from brainpy.brainpy import _IsotopicDistribution 7 | 8 | 9 | class TestIsotopicDistribution(unittest.TestCase): 10 | def test_neutral_mass(self): 11 | hexnac = {'H': 13, 'C': 8, 'O': 5, 'N': 1} 12 | dist = isotopic_variants(hexnac) 13 | 14 | reference = [ 15 | Peak(mz=203.079373, intensity=0.901867, charge=0), 16 | Peak(mz=204.082545, intensity=0.084396, charge=0), 17 | Peak(mz=205.084190, intensity=0.012787, charge=0), 18 | Peak(mz=206.086971, intensity=0.000950, charge=0) 19 | ] 20 | for inst, ref in zip(dist, reference): 21 | self.assertAlmostEqual(inst.mz, ref.mz, 3) 22 | self.assertAlmostEqual(inst.intensity, ref.intensity, 3) 23 | 24 | def test_bad_isotopic_specification(self): 25 | comp = {"Cm": 1} 26 | dist = isotopic_variants(comp) 27 | assert len(dist) == 1 28 | 29 | if _has_c: 30 | def test_pure_python(self): 31 | hexnac = {'H': 13, 'C': 8, 'O': 5, 'N': 1} 32 | dist = _IsotopicDistribution(hexnac, 4).aggregated_isotopic_variants() 33 | 34 | reference = [ 35 | Peak(mz=203.079373, intensity=0.901867, charge=0), 36 | Peak(mz=204.082545, intensity=0.084396, charge=0), 37 | Peak(mz=205.084190, intensity=0.012787, charge=0), 38 | Peak(mz=206.086971, intensity=0.000950, charge=0) 39 | ] 40 | for inst, ref in zip(dist, reference): 41 | self.assertAlmostEqual(inst.mz, ref.mz, 3) 42 | self.assertAlmostEqual(inst.intensity, ref.intensity, 3) 43 | 44 | 45 | if __name__ == '__main__': 46 | unittest.main() 47 | --------------------------------------------------------------------------------