├── .github └── workflows │ ├── publish.yml │ └── test-push.yml ├── .gitignore ├── .gitmodules ├── CHANGELOG.md ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── config.h ├── docs ├── Makefile └── source │ ├── _templates │ └── layout.html │ ├── classes.rst │ ├── conf.py │ ├── functions.rst │ ├── index.rst │ └── intro.rst ├── requirements.txt ├── setup.cfg ├── setup.py ├── src ├── cfreesasa.pxd ├── classifier.pyx ├── freesasa.pyx ├── parameters.pyx ├── result.pyx └── structure.pyx └── test.py /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish PyPi package for all platforms 2 | run-name: ${{ github.actor }} publishing freesasa-python 3 | on: 4 | release: 5 | types: [published] 6 | jobs: 7 | Publish: 8 | env: 9 | USE_CYTHON: 1 10 | TWINE_USERNAME: __token__ 11 | TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} 12 | strategy: 13 | matrix: 14 | os: [macos-latest, windows-latest] # we'll use the sdist for linux 15 | python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] 16 | exclude: 17 | - os: macos-latest 18 | python-version: 3.11 19 | runs-on: ${{ matrix.os }} 20 | steps: 21 | - name: "Check out code" 22 | uses: actions/checkout@v3 23 | - name: "Get upstream C library" 24 | run: git submodule update --init 25 | - name: Set up Python ${{ matrix.python-version }} 26 | uses: actions/setup-python@v4 27 | with: 28 | cache: "pip" 29 | python-version: ${{ matrix.python-version }} 30 | - name: Install dependencies 31 | run: | 32 | python -m pip install --upgrade pip setuptools 33 | pip install -r requirements.txt 34 | - name: Run tests 35 | run: python setup.py test 36 | - name: Build package 37 | run: python setup.py bdist_wheel 38 | - name: Build sdist 39 | # same source for all versions 40 | if: ${{ matrix.os == 'macos-latest' && matrix.python-version == '3.10' }} 41 | run: python setup.py sdist 42 | - name: Publish package on Mac 43 | if: ${{ matrix.os == 'macos-latest' }} 44 | run: twine upload --skip-existing dist/* 45 | - name: Publish package on Windows 46 | if: ${{ matrix.os == 'windows-latest' }} 47 | run: twine upload --skip-existing dist\* 48 | -------------------------------------------------------------------------------- /.github/workflows/test-push.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | run-name: ${{ github.actor }} testing freesasa-python 3 | on: 4 | push: 5 | branches: 6 | - "*" 7 | pull_request: 8 | branches: 9 | - master 10 | jobs: 11 | Test: 12 | env: 13 | USE_CYTHON: 1 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: "Check out code" 17 | uses: actions/checkout@v3 18 | - name: "Get upstream C library" 19 | run: git submodule update --init 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v4 22 | with: 23 | python-version: "3.11" 24 | cache: "pip" 25 | - name: Install dependencies 26 | run: | 27 | python -m pip install --upgrade pip 28 | pip install -r requirements.txt 29 | python setup.py install 30 | - name: Run tests 31 | run: python setup.py test 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | freesasa.egg-info 4 | *~ 5 | freesasa.c 6 | *.so 7 | __pycache__ 8 | test.pyc 9 | docs/build 10 | .eggs 11 | .DS_Store 12 | src/*.c -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib"] 2 | path = lib 3 | url = https://github.com/mittinatten/freesasa.git 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | # 2.2.0 4 | 5 | - Add `Result.write_pdb()` 6 | - Add `Structre.addAtoms()` 7 | - Fix bugs in `structureFromBioPDB()` where some options weren't handled properly 8 | - Add chain-groups support to `structureArray()`. 9 | 10 | # 2.1.0 11 | 12 | - Added changelog 13 | - Can access absolute and relative SASA for individual residues through `Result.residueAreas()` 14 | - Can set options and classifier for a `Structure` initiated without an input file for later 15 | use in `Structure.addAtom()` 16 | - Only build PyPi packages for Python 3.6+ (can still be built from source for older versions) 17 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Simon Mitternacht 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt config.h src/cfreesasa.pxd src/*.pyx 2 | 3 | include lib/src/lexer.h lib/src/classifier.h lib/src/pdb.h lib/src/freesasa.h \ 4 | lib/src/parser.h lib/src/coord.h lib/src/freesasa_internal.h lib/src/selection.h lib/src/nb.h 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FreeSASA Python module 2 | 3 | The module provides Python bindings for the [FreeSASA C Library](https://github.com/mittinatten/freesasa). 4 | There are PyPi packages for Python 3.7+, on Linux, Mac OS X and Windows. 5 | And it can be built from source for 2.7+ (Or by downloading older PyPi packages). 6 | Documentation can be found at http://freesasa.github.io/python/. 7 | 8 | Install the module by 9 | 10 | ```sh 11 | pip install freesasa 12 | ``` 13 | 14 | Or, alternatively, by using conda 15 | 16 | ```sh 17 | conda install -c conda-forge freesasa 18 | ``` 19 | 20 | Developers can clone the library, and then build the module by the following 21 | 22 | ```sh 23 | git submodule update --init 24 | USE_CYTHON=1 python setup.py build 25 | ``` 26 | 27 | Tests can be run using 28 | 29 | ```sh 30 | python setup.py test 31 | ``` 32 | 33 | # Adding new features 34 | 35 | This Python module provides a limited mapping to the C API of FreeSASA. 36 | I wish to extend the module with more functionality out of the box, 37 | to match the capabilities of the C API more closely, 38 | and perhaps also add more complex analysis that would be cumbersome to write in C. 39 | Feel free to submit feature request as GitHub issues. 40 | A few simple suggestions are already listed as issues. 41 | I only work on FreeSASA in my spare time, so PRs are always welcome. 42 | -------------------------------------------------------------------------------- /config.h: -------------------------------------------------------------------------------- 1 | /* Name of package */ 2 | #define PACKAGE "freesasa" 3 | 4 | /* Define to the full name of this package. */ 5 | #define PACKAGE_NAME "FreeSASA" 6 | 7 | /* Define to the full name and version of this package. */ 8 | #define PACKAGE_STRING "FreeSASA 2.0.2" 9 | 10 | /* Define to the version of this package. */ 11 | #define PACKAGE_VERSION "2.0.2" 12 | 13 | #define USE_XML 0 14 | 15 | #define USE_JSON 0 16 | 17 | #define USE_CHECK 0 18 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = FreeSASA 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @cd ../; USE_CYTHON=1 python3 setup.py build_ext --inplace; cd docs; 21 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 22 | -------------------------------------------------------------------------------- /docs/source/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "!layout.html" %} 2 | 3 | {% block footer %} 4 | {{ super() }} 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /docs/source/classes.rst: -------------------------------------------------------------------------------- 1 | Classes 2 | ======= 3 | 4 | Classifier 5 | ---------- 6 | 7 | .. autoclass:: freesasa.Classifier 8 | :members: 9 | :special-members: 10 | 11 | .. _config-files: http://freesasa.github.io/doxygen/Config-file.html 12 | 13 | Parameters 14 | ---------- 15 | 16 | .. autoclass:: freesasa.Parameters 17 | :members: 18 | :special-members: 19 | 20 | Result 21 | ------ 22 | 23 | .. autoclass:: freesasa.Result 24 | :members: 25 | 26 | ResidueArea 27 | ----------- 28 | .. autoclass:: freesasa.ResidueArea 29 | :members: 30 | 31 | Structure 32 | --------- 33 | 34 | .. autoclass:: freesasa.Structure 35 | :members: 36 | :special-members: 37 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/stable/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | import os 16 | import sys 17 | sys.path.insert(0, os.path.abspath('../../')) 18 | 19 | # -- Project information ----------------------------------------------------- 20 | 21 | project = 'FreeSASA Python Module' 22 | copyright = '2020, Simon Mitternacht' 23 | author = 'Simon Mitternacht' 24 | 25 | # The short X.Y version 26 | version = '2.2.0' 27 | # The full version, including alpha/beta/rc tags 28 | release = '2.2.0' 29 | 30 | 31 | # -- General configuration --------------------------------------------------- 32 | 33 | # If your documentation needs a minimal Sphinx version, state it here. 34 | # 35 | # needs_sphinx = '1.0' 36 | 37 | # Add any Sphinx extension module names here, as strings. They can be 38 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 39 | # ones. 40 | extensions = [ 41 | 'sphinx.ext.autodoc', 42 | 'sphinx.ext.ifconfig', 43 | 'sphinx.ext.githubpages', 44 | 'sphinx.ext.napoleon', 45 | 'sphinx.ext.autosummary' 46 | ] 47 | 48 | napoleon_google_docstring = True 49 | napoleon_use_param = False 50 | napoleon_use_ivar = True 51 | 52 | # Add any paths that contain templates here, relative to this directory. 53 | templates_path = ['_templates'] 54 | 55 | # The suffix(es) of source filenames. 56 | # You can specify multiple suffix as a list of string: 57 | # 58 | # source_suffix = ['.rst', '.md'] 59 | source_suffix = '.rst' 60 | 61 | # The master toctree document. 62 | master_doc = 'index' 63 | 64 | # The language for content autogenerated by Sphinx. Refer to documentation 65 | # for a list of supported languages. 66 | # 67 | # This is also used if you do content translation via gettext catalogs. 68 | # Usually you set "language" from the command line for these cases. 69 | language = None 70 | 71 | # List of patterns, relative to source directory, that match files and 72 | # directories to ignore when looking for source files. 73 | # This pattern also affects html_static_path and html_extra_path . 74 | exclude_patterns = [] 75 | 76 | # The name of the Pygments (syntax highlighting) style to use. 77 | pygments_style = 'sphinx' 78 | 79 | 80 | # -- Options for HTML output ------------------------------------------------- 81 | 82 | # The theme to use for HTML and HTML Help pages. See the documentation for 83 | # a list of builtin themes. 84 | # 85 | html_theme = 'sphinx_rtd_theme' 86 | 87 | # Theme options are theme-specific and customize the look and feel of a theme 88 | # further. For a list of options available for each theme, see the 89 | # documentation. 90 | # 91 | # html_theme_options = {} 92 | 93 | # Add any paths that contain custom static files (such as style sheets) here, 94 | # relative to this directory. They are copied after the builtin static files, 95 | # so a file named "default.css" will overwrite the builtin "default.css". 96 | html_static_path = ['_static'] 97 | 98 | # Custom sidebar templates, must be a dictionary that maps document names 99 | # to template names. 100 | # 101 | # The default sidebars (for documents that don't match any pattern) are 102 | # defined by theme itself. Builtin themes are using these templates by 103 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 104 | # 'searchbox.html']``. 105 | # 106 | # html_sidebars = {} 107 | 108 | 109 | # -- Options for HTMLHelp output --------------------------------------------- 110 | 111 | # Output file base name for HTML help builder. 112 | htmlhelp_basename = 'FreeSASAdoc' 113 | 114 | 115 | # -- Options for LaTeX output ------------------------------------------------ 116 | 117 | latex_elements = { 118 | # The paper size ('letterpaper' or 'a4paper'). 119 | # 120 | # 'papersize': 'letterpaper', 121 | 122 | # The font size ('10pt', '11pt' or '12pt'). 123 | # 124 | # 'pointsize': '10pt', 125 | 126 | # Additional stuff for the LaTeX preamble. 127 | # 128 | # 'preamble': '', 129 | 130 | # Latex figure (float) alignment 131 | # 132 | # 'figure_align': 'htbp', 133 | } 134 | 135 | # Grouping the document tree into LaTeX files. List of tuples 136 | # (source start file, target name, title, 137 | # author, documentclass [howto, manual, or own class]). 138 | latex_documents = [ 139 | (master_doc, 'FreeSASA.tex', 'FreeSASA Documentation', 140 | 'Simon Mitternacht', 'manual'), 141 | ] 142 | 143 | 144 | # -- Options for manual page output ------------------------------------------ 145 | 146 | # One entry per manual page. List of tuples 147 | # (source start file, name, description, authors, manual section). 148 | man_pages = [ 149 | (master_doc, 'freesasa', 'FreeSASA Documentation', 150 | [author], 1) 151 | ] 152 | 153 | 154 | # -- Options for Texinfo output ---------------------------------------------- 155 | 156 | # Grouping the document tree into Texinfo files. List of tuples 157 | # (source start file, target name, title, author, 158 | # dir menu entry, description, category) 159 | texinfo_documents = [ 160 | (master_doc, 'FreeSASA', 'FreeSASA Documentation', 161 | author, 'FreeSASA', 'One line description of project.', 162 | 'Miscellaneous'), 163 | ] 164 | 165 | 166 | # -- Extension configuration ------------------------------------------------- 167 | -------------------------------------------------------------------------------- /docs/source/functions.rst: -------------------------------------------------------------------------------- 1 | Functions 2 | ============================= 3 | .. currentmodule:: freesasa 4 | 5 | .. autosummary:: 6 | calc 7 | calcBioPDB 8 | calcCoord 9 | classifyResults 10 | getVerbosity 11 | selectArea 12 | setVerbosity 13 | 14 | .. autofunction:: calc 15 | .. autofunction:: calcBioPDB 16 | .. autofunction:: calcCoord 17 | .. autofunction:: classifyResults 18 | .. autofunction:: getVerbosity 19 | .. autofunction:: setVerbosity 20 | .. autofunction:: selectArea 21 | .. autofunction:: structureArray 22 | .. autofunction:: structureFromBioPDB 23 | 24 | .. _select-syntax: http://freesasa.github.io/doxygen/Selection.html 25 | .. _C API: http://freesasa.github.io/doxygen/API.html 26 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. toctree:: 2 | :maxdepth: 2 3 | 4 | intro 5 | functions 6 | classes 7 | 8 | FreeSASA Python Module 9 | ====================== 10 | 11 | The module provides Python bindings for the `FreeSASA C Library `_. 12 | Python 3.7+, on Linux, Mac OS X and Windows are officially supported (it will probably still run on older Python 13 | versions if you build it from source, or use older PyPi packages). 14 | The source is available as a PyPi source 15 | distribution and on `GitHub `_. 16 | 17 | Install the FreeSASA Python Module by 18 | 19 | .. code:: 20 | 21 | pip install freesasa 22 | 23 | 24 | Developers can clone the library, and then build the module by the following 25 | 26 | .. code:: 27 | 28 | git clone https://github.com/freesasa/freesasa-python.git 29 | git submodule update --init 30 | python setyp.py build 31 | 32 | Tests are run by 33 | 34 | .. code:: 35 | 36 | python setup.py test 37 | -------------------------------------------------------------------------------- /docs/source/intro.rst: -------------------------------------------------------------------------------- 1 | .. currentmodule:: freesasa 2 | 3 | Introduction 4 | ============ 5 | 6 | This package provides Python bindings for the `FreeSASA C Library 7 | `_. 8 | 9 | It can be installed using 10 | 11 | .. code:: 12 | 13 | pip install freesasa 14 | 15 | Binaries are available for Python 3.7-3.11 for Mac OS X 16 | and Windows, in addition to the source distribution. 17 | 18 | 19 | Basic calculations 20 | ------------------ 21 | 22 | Using defaults everywhere a simple calculation can be carried out as 23 | follows (assuming the file ``1ubq.pdb`` is available) 24 | 25 | .. code:: python 26 | 27 | import freesasa 28 | 29 | structure = freesasa.Structure("1ubq.pdb") 30 | result = freesasa.calc(structure) 31 | area_classes = freesasa.classifyResults(result, structure) 32 | 33 | print "Total : %.2f A2" % result.totalArea() 34 | for key in area_classes: 35 | print key, ": %.2f A2" % area_classes[key] 36 | 37 | Which would give the following output 38 | 39 | .. code:: 40 | 41 | Total : 4804.06 A2 42 | Polar : 2504.22 A2 43 | Apolar : 2299.84 A2 44 | 45 | The following does a high precision L&R calculation 46 | 47 | .. code:: python 48 | 49 | result = freesasa.calc(structure, 50 | freesasa.Parameters({'algorithm' : freesasa.LeeRichards, 51 | 'n-slices' : 100})) 52 | 53 | Using the results from a calculation we can also integrate SASA over a selection of 54 | atoms, using a subset of the Pymol `selection syntax`_: 55 | 56 | .. _selection syntax: http://freesasa.github.io/doxygen/Selection.html 57 | 58 | .. code:: python 59 | 60 | selections = freesasa.selectArea(('alanine, resn ala', 'r1_10, resi 1-10'), 61 | structure, result) 62 | for key in selections: 63 | print key, ": %.2f A2" % selections[key] 64 | 65 | which gives the output 66 | 67 | .. code:: 68 | 69 | alanine : 120.08 A2 70 | r1_10 : 634.31 A2 71 | 72 | Customizing atom classification 73 | ------------------------------- 74 | 75 | This uses the NACCESS parameters (the file ``naccess.config`` is 76 | available in the ``share/`` directory of the repository). 77 | 78 | .. code:: python 79 | 80 | classifier = freesasa.Classifier("naccess.config") 81 | structure = freesasa.Structure("1ubq.pdb", classifier) 82 | result = freesasa.calc(structure) 83 | area_classes = freesasa.classifyResults(result, structure, classifier) 84 | 85 | Classification can be customized also by extending the :py:class:`.Classifier` 86 | interface. The code below is an illustration of a classifier that 87 | classes nitrogens separately, and assigns radii based on element only 88 | (and crudely). 89 | 90 | .. code:: python 91 | 92 | import freesasa 93 | import re 94 | 95 | class DerivedClassifier(freesasa.Classifier): 96 | # this must be set explicitly in all derived classifiers 97 | purePython = True 98 | 99 | def classify(self, residueName, atomName): 100 | if re.match('\s*N', atomName): 101 | return 'Nitrogen' 102 | return 'Not-nitrogen' 103 | 104 | def radius(self, residueName, atomName): 105 | if re.match('\s*N',atomName): # Nitrogen 106 | return 1.6 107 | if re.match('\s*C',atomName): # Carbon 108 | return 1.7 109 | if re.match('\s*O',atomName): # Oxygen 110 | return 1.4 111 | if re.match('\s*S',atomName): # Sulfur 112 | return 1.8 113 | return 0; # everything else (Hydrogen, etc) 114 | 115 | classifier = DerivedClassifier() 116 | 117 | # use the DerivedClassifier to calculate atom radii 118 | structure = freesasa.Structure("1ubq.pdb", classifier) 119 | result = freesasa.calc(structure) 120 | 121 | # use the DerivedClassifier to classify atoms 122 | area_classes = freesasa.classifyResults(result,structure,classifier) 123 | 124 | Of course, this example is somewhat contrived, if we only want the 125 | integrated area of nitrogen atoms, the simpler choice would be 126 | 127 | .. code:: python 128 | 129 | selection = freesasa.selectArea('nitrogen, symbol n', structure, result) 130 | 131 | 132 | However, extending :py:class:`.Classifier`, as illustrated above, allows 133 | classification to arbitrary complexity and also lets us redefine the 134 | radii used in the calculation. 135 | 136 | Bio.PDB 137 | ------- 138 | 139 | FreeSASA can also calculate the SASA of a ``Bio.PDB`` structure from BioPython 140 | 141 | .. code:: python 142 | 143 | from Bio.PDB import PDBParser 144 | parser = PDBParser() 145 | structure = parser.get_structure("Ubiquitin", "1ubq.pdb") 146 | result, sasa_classes = freesasa.calcBioPDB(structure) 147 | 148 | If one needs more control over the analysis the structure can be 149 | converted to a :py:class:`.Structure` using :py:func:`.structureFromBioPDB()` 150 | and the calculation can be performed the normal way using this 151 | structure. 152 | 153 | Writing a FreeSASA PDB 154 | ---------------------- 155 | 156 | Here is a simple example on how to turn a calculated result into a PDB file. 157 | 158 | 159 | .. code:: python 160 | 161 | import freesasa 162 | 163 | structure = freesasa.Structure('2ubq.pdb') 164 | result = freesasa.calc(structure) 165 | result.write_pdb('2ubq.sasa.pdb') 166 | 167 | This only works if the input file was parsed with `Freesasa.Structure()`. 168 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | twine 2 | cython 3 | biopython 4 | wheel 5 | nose 6 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freesasa/freesasa-python/7ead59e34ebe456b7ed27682455c6bf5bd0e7de7/setup.cfg -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, Extension 2 | import os 3 | import sys 4 | from glob import glob 5 | 6 | USE_CYTHON = False 7 | 8 | try: 9 | USE_CYTHON = os.environ['USE_CYTHON'] 10 | except KeyError: 11 | if not os.path.isfile(os.path.join("src", "freesasa.c")): 12 | sys.stderr.write("No C source detected, define environment variable USE_CYTHON to build from Cython source.\n") 13 | sys.exit() 14 | else: 15 | print ("Define environment variable USE_CYTHON to build from Cython source") 16 | 17 | 18 | # not using wild cards because we're leaving out xml and json 19 | sources = list(map(lambda file: os.path.join('lib', 'src', file), 20 | ["classifier.c", 21 | "classifier_protor.c", "classifier_oons.c", "classifier_naccess.c", 22 | "coord.c", "freesasa.c", "lexer.c", "log.c", 23 | "nb.c", "node.c", "parser.c", 24 | "pdb.c", "rsa.c", "sasa_lr.c", "sasa_sr.c", 25 | "selection.c", "structure.c", 26 | "util.c"])) 27 | 28 | 29 | extensions = None 30 | 31 | ext = '.pyx' if USE_CYTHON else '.c' 32 | sources.append(os.path.join('src', 'freesasa' + ext)) 33 | 34 | compile_args=['-DHAVE_CONFIG_H'] 35 | 36 | if os.name == 'posix': 37 | compile_args.append('-std=gnu99') 38 | 39 | extension_src = [ 40 | Extension("freesasa", sources, 41 | language='c', 42 | include_dirs=[os.path.join('lib', 'src'), '.'], 43 | extra_compile_args = compile_args 44 | ) 45 | ] 46 | 47 | if USE_CYTHON: 48 | from Cython.Build import cythonize, build_ext 49 | extensions = cythonize(extension_src) 50 | else: 51 | extensions = extension_src 52 | 53 | long_description = \ 54 | "This module provides bindings for the FreeSASA C library. " + \ 55 | "See http://freesasa.github.io/python/ for documentation." 56 | 57 | setup( 58 | name='freesasa', 59 | version= '2.2.1', 60 | description='Calculate solvent accessible surface areas of proteins', 61 | long_description=long_description, 62 | author='Simon Mitternacht', 63 | url='http://freesasa.github.io/', 64 | license='MIT', 65 | ext_modules=extensions, 66 | keywords=['structural biology', 'proteins', 'bioinformatics'], 67 | headers=glob(os.path.join('lib', 'src', '*')), 68 | classifiers=[ 69 | 'Development Status :: 5 - Production/Stable', 70 | 'Topic :: Scientific/Engineering :: Bio-Informatics', 71 | 'Topic :: Scientific/Engineering :: Chemistry', 72 | 'License :: OSI Approved :: MIT License', 73 | 'Programming Language :: Python :: 3.7', 74 | 'Programming Language :: Python :: 3.8', 75 | 'Programming Language :: Python :: 3.9', 76 | 'Programming Language :: Python :: 3.10', 77 | 'Programming Language :: Python :: 3.11', 78 | # Will also build for python 2.7 but not officialy supported 79 | ], 80 | setup_requires=['cython>=0.29.13'], 81 | test_suite='test' 82 | ) 83 | -------------------------------------------------------------------------------- /src/cfreesasa.pxd: -------------------------------------------------------------------------------- 1 | from libc.stdio cimport FILE 2 | 3 | cdef extern from "freesasa.h": 4 | ctypedef enum freesasa_algorithm: 5 | FREESASA_LEE_RICHARDS, FREESASA_SHRAKE_RUPLEY 6 | 7 | ctypedef enum freesasa_verbosity: 8 | FREESASA_V_NORMAL, FREESASA_V_NOWARNINGS, FREESASA_V_SILENT, FREESASA_V_DEBUG 9 | 10 | ctypedef enum freesasa_atom_class: 11 | FREESASA_ATOM_APOLAR, FREESASA_ATOM_POLAR, FREESASA_ATOM_UNKNOWN 12 | 13 | cdef const int FREESASA_SUCCESS 14 | cdef const int FREESASA_FAIL 15 | cdef const int FREESASA_WARN 16 | 17 | cdef const int FREESASA_INCLUDE_HETATM 18 | cdef const int FREESASA_INCLUDE_HYDROGEN 19 | cdef const int FREESASA_SEPARATE_CHAINS 20 | cdef const int FREESASA_SEPARATE_MODELS 21 | cdef const int FREESASA_JOIN_MODELS 22 | cdef const int FREESASA_HALT_AT_UNKNOWN 23 | cdef const int FREESASA_SKIP_UNKNOWN 24 | 25 | cdef const int FREESASA_MAX_SELECTION_NAME 26 | 27 | ctypedef struct freesasa_parameters: 28 | freesasa_algorithm alg 29 | double probe_radius 30 | int shrake_rupley_n_points 31 | int lee_richards_n_slices 32 | int n_threads 33 | 34 | ctypedef struct freesasa_result: 35 | double total 36 | double *sasa 37 | int n_atoms 38 | 39 | ctypedef struct freesasa_nodearea: 40 | const char *name 41 | double total 42 | double main_chain 43 | double side_chain 44 | double polar 45 | double apolar 46 | double unknown 47 | 48 | ctypedef struct freesasa_node: 49 | pass 50 | 51 | ctypedef enum freesasa_nodetype: 52 | pass 53 | 54 | ctypedef struct freesasa_classifier: 55 | pass 56 | 57 | ctypedef struct freesasa_structure: 58 | pass 59 | 60 | ctypedef struct freesasa_selection: 61 | pass 62 | 63 | cdef extern const freesasa_parameters freesasa_default_parameters 64 | cdef extern const freesasa_classifier freesasa_default_classifier 65 | cdef extern const freesasa_classifier freesasa_residue_classifier 66 | cdef extern const freesasa_classifier freesasa_naccess_classifier 67 | cdef extern const freesasa_classifier freesasa_protor_classifier 68 | cdef extern const freesasa_classifier freesasa_oons_classifier 69 | 70 | freesasa_result* freesasa_calc_structure(const freesasa_structure *structure, 71 | const freesasa_parameters *parameters) 72 | 73 | freesasa_result* freesasa_calc_coord(const double *xyz, 74 | const double *radii, 75 | int n, 76 | const freesasa_parameters *parameters) 77 | 78 | void freesasa_result_free(freesasa_result *result) 79 | 80 | freesasa_classifier* freesasa_classifier_from_file(FILE *file) 81 | 82 | void freesasa_classifier_free(freesasa_classifier *classifier) 83 | 84 | int freesasa_structure_chain_residues(const freesasa_structure *structure, 85 | char chain, 86 | int *first, 87 | int *last) 88 | 89 | double freesasa_classifier_radius(const freesasa_classifier *classifier, 90 | const char *res_name, 91 | const char *atom_name) 92 | 93 | freesasa_atom_class freesasa_classifier_class(const freesasa_classifier *classifier, 94 | const char *res_name, 95 | const char *atom_name) 96 | 97 | const char* freesasa_classifier_class2str(freesasa_atom_class the_class) 98 | 99 | freesasa_selection * freesasa_selection_new(const char *command, 100 | const freesasa_structure *structure, 101 | const freesasa_result *result) 102 | 103 | void freesasa_selection_free(freesasa_selection *selection) 104 | 105 | const char * freesasa_selection_name(const freesasa_selection* selection) 106 | 107 | const char * freesasa_selection_command(const freesasa_selection* selection) 108 | 109 | double freesasa_selection_area(const freesasa_selection* selection) 110 | 111 | int freesasa_selection_n_atoms(const freesasa_selection* selection) 112 | 113 | int freesasa_write_pdb(FILE *output, 114 | freesasa_result *result, 115 | const freesasa_structure *structure) 116 | 117 | int freesasa_per_residue_type(FILE *output, 118 | freesasa_result *result, 119 | const freesasa_structure *structure) 120 | 121 | int freesasa_per_residue(FILE *output, 122 | freesasa_result *result, 123 | const freesasa_structure *structure) 124 | 125 | int freesasa_set_verbosity(freesasa_verbosity v) 126 | 127 | freesasa_verbosity freesasa_get_verbosity() 128 | 129 | freesasa_structure* freesasa_structure_from_pdb(FILE *pdb, 130 | const freesasa_classifier* classifier, 131 | int options) 132 | 133 | freesasa_structure** freesasa_structure_array(FILE *pdb, 134 | int *n, 135 | const freesasa_classifier* classifier, 136 | int options) 137 | 138 | freesasa_structure* freesasa_structure_get_chains(const freesasa_structure *structure, 139 | const char *chains, 140 | const freesasa_classifier *classifier, 141 | int options) 142 | 143 | freesasa_structure* freesasa_structure_new() 144 | 145 | int freesasa_structure_n(freesasa_structure *structure) 146 | 147 | void freesasa_structure_free(freesasa_structure* structure) 148 | 149 | const double* freesasa_structure_radius(const freesasa_structure *structure) 150 | 151 | void freesasa_structure_set_radius(freesasa_structure *structure, 152 | const double *radii) 153 | 154 | 155 | int freesasa_structure_add_atom(freesasa_structure *structure, 156 | const char* atom_name, 157 | const char* residue_name, 158 | const char* residue_number, 159 | char chain_label, 160 | double x, double y, double z) 161 | 162 | int freesasa_structure_add_atom_wopt(freesasa_structure *structure, 163 | const char* atom_name, 164 | const char* residue_name, 165 | const char* residue_number, 166 | char chain_label, 167 | double x, double y, double z, 168 | const freesasa_classifier *classifier, 169 | int options) 170 | 171 | const char* freesasa_structure_atom_name(const freesasa_structure *structure, 172 | int i) 173 | 174 | const char* freesasa_structure_atom_res_name(const freesasa_structure *structure, 175 | int i) 176 | 177 | const char* freesasa_structure_atom_res_number(const freesasa_structure *structure, 178 | int i) 179 | 180 | double freesasa_structure_atom_radius(const freesasa_structure *structure, 181 | int i) 182 | 183 | void freesasa_structure_atom_set_radius(const freesasa_structure *structure, 184 | int i, 185 | double radius) 186 | 187 | char freesasa_structure_atom_chain(const freesasa_structure *structure, int i) 188 | 189 | const double* freesasa_structure_coord_array(const freesasa_structure *structure) 190 | 191 | 192 | freesasa_nodearea freesasa_result_classes(const freesasa_structure *structure, 193 | const freesasa_result *result) 194 | 195 | freesasa_node * freesasa_tree_init(const freesasa_result *result, 196 | const freesasa_structure *structure, 197 | const char *name) 198 | 199 | int freesasa_node_free(freesasa_node *root) 200 | 201 | const freesasa_nodearea * freesasa_node_area(const freesasa_node *node) 202 | 203 | freesasa_node * freesasa_node_children(freesasa_node *node) 204 | 205 | freesasa_node * freesasa_node_next(freesasa_node *node) 206 | 207 | freesasa_node * freesasa_node_parent(freesasa_node *node) 208 | 209 | freesasa_nodetype freesasa_node_type(const freesasa_node *node) 210 | 211 | const char * freesasa_node_name(const freesasa_node *node) 212 | 213 | const char * freesasa_node_classified_by(const freesasa_node *node) 214 | 215 | int freesasa_node_atom_is_polar(const freesasa_node *node) 216 | 217 | int freesasa_node_atom_is_mainchain(const freesasa_node *node) 218 | 219 | double freesasa_node_atom_radius(const freesasa_node *node) 220 | 221 | const char * freesasa_node_atom_pdb_line(const freesasa_node *node) 222 | 223 | const char * freesasa_node_residue_number(const freesasa_node *node) 224 | 225 | int freesasa_node_residue_n_atoms(const freesasa_node *node) 226 | 227 | const freesasa_nodearea * freesasa_node_residue_reference(const freesasa_node *node) 228 | 229 | int freesasa_node_chain_n_residues(const freesasa_node *node) 230 | 231 | int freesasa_node_structure_n_chains(const freesasa_node *node) 232 | 233 | int freesasa_node_structure_n_atoms(const freesasa_node *node) 234 | 235 | const char * freesasa_node_structure_chain_labels(const freesasa_node *node) 236 | 237 | int freesasa_node_structure_model(const freesasa_node *node) 238 | 239 | const freesasa_result * freesasa_node_structure_result(const freesasa_node *node) 240 | 241 | cdef extern from "freesasa_internal.h": 242 | int freesasa_write_pdb(FILE *output, freesasa_node *structure) 243 | -------------------------------------------------------------------------------- /src/classifier.pyx: -------------------------------------------------------------------------------- 1 | from cfreesasa cimport * 2 | from libc.stdio cimport FILE, fopen, fclose 3 | 4 | cdef class Classifier: 5 | """ 6 | Assigns class and radius to atom by residue and atom name. 7 | 8 | Subclasses derived from :py:class:`.Classifier` can be used to define custom 9 | atomic radii and/or classes. Can also be initialized from 10 | config-files_ with a custom classifier. 11 | 12 | If initialized without arguments the default classifier is used. 13 | 14 | Derived classifiers must set the member :py:attr:`.purePython` to ``True`` 15 | 16 | Residue names should be of the format ``"ALA"``, ``"ARG"``, etc. 17 | Atom names should be of the format ``"CA"``, ``"N"``, etc. 18 | """ 19 | # this reference is used for classification 20 | cdef const freesasa_classifier *_c_classifier 21 | 22 | # if the classifier is read from a file we store it here, 23 | # with a reference in _c_classifier (for the sake of const-correctness) 24 | cdef freesasa_classifier *_dynamic_c_classifier 25 | 26 | # to be used by derived classes 27 | purePython = False 28 | 29 | def __init__ (self, fileName=None): 30 | """Constructor. 31 | 32 | If no file is provided the default classifier is used. 33 | 34 | Args: 35 | fileName (str): Name of file with classifier configuration. 36 | 37 | Raises: 38 | IOError: Problem opening/reading file 39 | Exception: Problem parsing provided configuration or 40 | initializing defaults 41 | """ 42 | cdef FILE *config 43 | 44 | self._c_classifier = NULL 45 | self._dynamic_c_classifier = NULL 46 | 47 | if fileName is not None: 48 | config = fopen(fileName, 'rb') 49 | if config is NULL: 50 | raise IOError("File '%s' could not be opened." % fileName) 51 | self._dynamic_c_classifier = freesasa_classifier_from_file(config) 52 | fclose(config) 53 | self._c_classifier = self._dynamic_c_classifier; 54 | if self._c_classifier is NULL: 55 | raise Exception("Error parsing configuration in '%s'." % fileName) 56 | 57 | else: 58 | self._c_classifier = &freesasa_default_classifier 59 | 60 | # The destructor 61 | def __dealloc__(self): 62 | if (self._isCClassifier()): 63 | freesasa_classifier_free(self._dynamic_c_classifier) 64 | 65 | @staticmethod 66 | def getStandardClassifier(type): 67 | """ 68 | Get a standard classifier (ProtOr, OONS or NACCESS) 69 | 70 | Args: 71 | type (str): The type, can have values ``'protor'``, ``'oons'`` or ``'naccess'`` 72 | 73 | Returns: 74 | :py:class:`.Classifier`: The requested classifier 75 | 76 | Raises: 77 | Exception: If type not recognized 78 | """ 79 | classifier = Classifier() 80 | if type == 'naccess': 81 | classifier._c_classifier = &freesasa_naccess_classifier 82 | elif type == 'oons': 83 | classifier._c_classifier = &freesasa_oons_classifier 84 | elif type == 'protor': 85 | classifier._c_classifier = &freesasa_protor_classifier 86 | else: 87 | raise Exception("Uknown classifier '%s'" % type) 88 | return classifier 89 | 90 | # This is used internally to determine if a Classifier wraps a C 91 | # classifier or not (necessary when generating structures) 92 | # returns Boolean 93 | def _isCClassifier(self): 94 | return not self.purePython 95 | 96 | def classify(self, residueName, atomName): 97 | """Class of atom. 98 | 99 | Depending on the configuration these classes can be 100 | anything, but typically they will be ``"Polar"`` and ``"Apolar"``. 101 | Unrecognized atoms will get the class ``"Unknown"``. 102 | 103 | Args: 104 | residueName (str): Residue name (`"ALA"`, `"ARG"`,...). 105 | atomName (str): Atom name (`"CA"`, `"C"`,...). 106 | 107 | Returns: 108 | str: Class name 109 | """ 110 | classIndex = freesasa_classifier_class(self._c_classifier, residueName, atomName) 111 | return freesasa_classifier_class2str(classIndex) 112 | 113 | def radius(self,residueName,atomName): 114 | """Radius of atom. 115 | 116 | This allows the classifier to be used to calculate the atomic 117 | radii used in calculations. Unknown atoms will get a negative 118 | radius. 119 | 120 | Args: 121 | residueName (str): Residue name (`"ALA"`, `"ARG"`, ...). 122 | atomName (str): Atom name (`"CA"`, `"C"`, ...). 123 | 124 | Returns: 125 | float: The radius in Å. 126 | """ 127 | return freesasa_classifier_radius(self._c_classifier, residueName, atomName) 128 | 129 | # the address obtained is a pointer to const 130 | def _get_address(self, size_t ptr2ptr): 131 | cdef freesasa_classifier **p = ptr2ptr 132 | p[0] = self._c_classifier # const cast 133 | -------------------------------------------------------------------------------- /src/freesasa.pyx: -------------------------------------------------------------------------------- 1 | # -*- mode: python; python-indent-offset: 4 -*- 2 | # 3 | # The cython directives will fail if we don't have a few lines of comments above them. (Why?) 4 | # 5 | # cython: c_string_type=str, c_string_encoding=ascii 6 | 7 | """ 8 | The :py:mod:`freesasa` python module wraps the FreeSASA `C API`_ 9 | """ 10 | 11 | from libc.stdio cimport FILE, fopen, fclose 12 | from libc.stdlib cimport free, realloc, malloc 13 | from libc.string cimport memcpy 14 | from cfreesasa cimport * 15 | 16 | include "parameters.pyx" 17 | include "result.pyx" 18 | include "classifier.pyx" 19 | include "structure.pyx" 20 | 21 | ## Used for classification 22 | polar = 'Polar' 23 | 24 | ## Used for classification 25 | apolar = 'Apolar' 26 | 27 | ## int: Suppress all warnings and errors (used by setVerbosity()) 28 | silent = FREESASA_V_SILENT 29 | 30 | ## int: Suppress all warnings but not errors (used by setVerbosity()) 31 | nowarnings = FREESASA_V_NOWARNINGS 32 | 33 | ## int: Normal verbosity (used by setVerbosity()) 34 | normal = FREESASA_V_NORMAL 35 | 36 | ## int: Print debug messages (used by setVerbosity()) 37 | debug = FREESASA_V_DEBUG 38 | 39 | 40 | def calc(structure,parameters=None): 41 | """ 42 | Calculate SASA of Structure 43 | 44 | Args: 45 | structure: :py:class:`.Structure` to be used 46 | parameters: :py:class:`.Parameters` to use (if not specified defaults are used) 47 | 48 | Returns: 49 | :py:class:`.Result`: The results 50 | 51 | Raises: 52 | Exception: something went wrong in calculation (see C library error messages) 53 | """ 54 | cdef const freesasa_parameters *p = NULL 55 | cdef const freesasa_structure *s = NULL 56 | if parameters is not None: parameters._get_address(&p) 57 | structure._get_address(&s) 58 | result = Result() 59 | result._c_result = freesasa_calc_structure(s,p) 60 | result._c_root_node = freesasa_tree_init(result._c_result, 61 | s, "Structure") 62 | if result._c_result is NULL: 63 | raise Exception("Error calculating SASA.") 64 | 65 | return result 66 | 67 | def calcCoord(coord, radii, parameters=None): 68 | """ 69 | Calculate SASA for a set of coordinates and radii 70 | 71 | Args: 72 | coord (list): array of size 3*N with atomic coordinates 73 | `(x1, y1, z1, x2, y2, z2, ..., x_N, y_N, z_N)`. 74 | radii (list): array of size N with atomic radii `(r_1, r_2, ..., r_N)`. 75 | parameters: :py:class:`.Parameters` to use (if not specified, defaults are used) 76 | Raises: 77 | AssertionError: mismatched array-sizes 78 | Exception: Out of memory 79 | Exception: something went wrong in calculation (see C library error messages) 80 | """ 81 | assert(len(coord) == 3*len(radii)) 82 | 83 | cdef const freesasa_parameters *p = NULL 84 | cdef double *c = malloc(len(coord)*sizeof(double)) 85 | cdef double *r = malloc(len(radii)*sizeof(double)) 86 | if c is NULL or r is NULL: 87 | raise Exception("Memory allocation error") 88 | 89 | for i in xrange(len(coord)): 90 | c[i] = coord[i] 91 | for i in xrange(len(radii)): 92 | r[i] = radii[i] 93 | 94 | if parameters is not None: parameters._get_address(&p) 95 | 96 | result = Result() 97 | result._c_result = freesasa_calc_coord(c, r, len(radii), p) 98 | 99 | if result._c_result is NULL: 100 | raise Exception("Error calculating SASA.") 101 | 102 | free(c) 103 | free(r) 104 | 105 | return result 106 | 107 | def classifyResults(result,structure,classifier=None): 108 | """ 109 | Break SASA result down into classes. 110 | 111 | Args: 112 | result: :py:class:`.Result` from SASA calculation. 113 | structure: :py:class:`Structure` used in calculation. 114 | classifier: :py:class:`.Classifier` to use (if not specified default is used). 115 | 116 | Returns: 117 | dict: Dictionary with names of classes as keys and their SASA values as values. 118 | 119 | Raises: 120 | Exception: Problems with classification, see C library error messages 121 | (or Python exceptions if run with derived classifier). 122 | """ 123 | if classifier is None: 124 | classifier = Classifier() 125 | ret = dict() 126 | for i in range(0,structure.nAtoms()): 127 | name = classifier.classify(structure.residueName(i),structure.atomName(i)) 128 | if name not in ret: 129 | ret[name] = 0 130 | ret[name] += result.atomArea(i) 131 | return ret 132 | 133 | def selectArea(commands, structure, result): 134 | """ 135 | Sum SASA result over a selection of atoms 136 | 137 | Args: 138 | commands (list): A list of commands with selections using Pymol 139 | syntax, e.g. ``"s1, resn ala+arg"`` or ``"s2, chain A and resi 1-5"``. 140 | See `select-syntax`_. 141 | structure: A :py:class:`.Structure`. 142 | result: :py:class:`.Result` from sasa calculation on structure. 143 | 144 | Returns: 145 | dict: Dictionary with names of selections (``"s1"``, ``"s2"``, ...) as 146 | keys, and the corresponding SASA values as values. 147 | 148 | Raises: 149 | Exception: Parser failed (typically syntax error), see 150 | library error messages. 151 | """ 152 | cdef freesasa_structure *s 153 | cdef freesasa_result *r 154 | cdef freesasa_selection *selection 155 | structure._get_address( &s) 156 | result._get_address( &r) 157 | value = dict() 158 | for cmd in commands: 159 | selection = freesasa_selection_new(cmd, s, r) 160 | if selection == NULL: 161 | raise Exception("Error parsing '%s'" % cmd) 162 | value[freesasa_selection_name(selection)] = freesasa_selection_area(selection) 163 | freesasa_selection_free(selection) 164 | return value 165 | 166 | def setVerbosity(verbosity): 167 | """ 168 | Set global verbosity 169 | 170 | Args: 171 | verbosity (int): Can have values :py:const:`.silent`, :py:const:`.nowarnings` 172 | or :py:const:`.normal` 173 | Raises: 174 | AssertionError: if verbosity has illegal value 175 | """ 176 | assert(verbosity in [silent, nowarnings, normal]) 177 | freesasa_set_verbosity(verbosity) 178 | 179 | 180 | def getVerbosity(): 181 | """ 182 | Get global verbosity 183 | 184 | Returns: 185 | int: Verbosity :py:const:`.silent`, :py:const:`.nowarnings` 186 | or :py:const:`.normal` 187 | """ 188 | return freesasa_get_verbosity() 189 | 190 | def calcBioPDB(bioPDBStructure, parameters = Parameters(), 191 | classifier = None, options = Structure.defaultOptions): 192 | """ 193 | Calc SASA from `BioPython` PDB structure. 194 | 195 | Usage:: 196 | 197 | result, sasa_classes, residue_areas = calcBioPDB(structure, ...) 198 | 199 | Experimental, not thorougly tested yet 200 | 201 | Args: 202 | bioPDBStructure: A `Bio.PDB` structure 203 | parameters: A :py:class:`.Parameters` object (uses default if none specified) 204 | classifier: A :py:class:`.Classifier` object (uses default if none specified) 205 | options (dict): Options supported are 'hetatm', 'skip-unknown' and 'halt-at-unknown' 206 | (uses :py:attr:`.Structure.defaultOptions` if none specified 207 | 208 | Returns: 209 | A :py:class:`.Result` object, a dictionary with classes 210 | defined by the classifier and associated areas, 211 | and a dictionary of the type returned by :py:meth:`.Result.residueAreas`. 212 | 213 | Raises: 214 | Exception: if unknown atom is encountered and the option 215 | 'halt-at-unknown' is active. Passes on exceptions from 216 | :py:func:`.calc()`, :py:func:`.classifyResults()` and 217 | :py:func:`.structureFromBioPDB()`. 218 | """ 219 | structure = structureFromBioPDB(bioPDBStructure, classifier, options) 220 | result = calc(structure, parameters) 221 | 222 | # Hack!: 223 | # This calculation depends on the structure not having been deallocated, 224 | # calling it later will cause seg-faults. By calling it now the result 225 | # is stored. 226 | # TODO: See if there is a refactoring that solves this in a more elegant way 227 | # residue_areas = result.residueAreas() 228 | 229 | sasa_classes = classifyResults(result, structure, classifier) 230 | return result, sasa_classes #, residue_areas 231 | -------------------------------------------------------------------------------- /src/parameters.pyx: -------------------------------------------------------------------------------- 1 | from cfreesasa cimport * 2 | 3 | ## Used to specify the algorithm by Shrake & Rupley 4 | ShrakeRupley = 'ShrakeRupley' 5 | 6 | ## Used to specify the algorithm by Lee & Richards 7 | LeeRichards = 'LeeRichards' 8 | 9 | cdef class Parameters: 10 | """ 11 | Stores parameter values to be used by calculation. 12 | 13 | Default parameters are :: 14 | 15 | Parameters.defaultParameters = { 16 | 'algorithm' : LeeRichards, 17 | 'probe-radius' : freesasa_default_parameters.probe_radius, 18 | 'n-points' : freesasa_default_parameters.shrake_rupley_n_points, 19 | 'n-slices' : freesasa_default_parameters.lee_richards_n_slices, 20 | 'n-threads' : freesasa_default_parameters.n_threads 21 | } 22 | 23 | Attributes: 24 | defaultParamers (dict): The default parameters 25 | """ 26 | 27 | 28 | cdef freesasa_parameters _c_param 29 | 30 | defaultParameters = { 31 | 'algorithm' : LeeRichards, 32 | 'probe-radius' : freesasa_default_parameters.probe_radius, 33 | 'n-points' : freesasa_default_parameters.shrake_rupley_n_points, 34 | 'n-slices' : freesasa_default_parameters.lee_richards_n_slices, 35 | 'n-threads' : freesasa_default_parameters.n_threads 36 | } 37 | 38 | def __init__(self,param=None): 39 | """ 40 | Initializes Parameters object. 41 | 42 | Args: 43 | param (dict): optional argument to specify parameter-values, 44 | see :py:attr:`.Parameters.defaultParameters`. 45 | 46 | Raises: 47 | AssertionError: Invalid parameter values supplied 48 | """ 49 | self._c_param = freesasa_default_parameters 50 | if param != None: 51 | if 'algorithm' in param: self.setAlgorithm(param['algorithm']) 52 | if 'probe-radius' in param: self.setProbeRadius(param['probe-radius']) 53 | if 'n-points' in param: self.setNPoints(param['n-points']) 54 | if 'n-slices' in param: self.setNSlices(param['n-slices']) 55 | if 'n-threads' in param: self.setNThreads(param['n-threads']) 56 | unknownKeys = [] 57 | for key in param: 58 | if not key in self.defaultParameters: 59 | unknownKeys.append(key) 60 | if len(unknownKeys) > 0: 61 | raise AssertionError('Key(s): ',unknownKeys,', unknown') 62 | 63 | def setAlgorithm(self,alg): 64 | """ 65 | Set algorithm. 66 | 67 | Args: 68 | alg (str): algorithm name, only allowed values are 69 | :py:data:`freesasa.ShrakeRupley` and :py:data:`freesasa.LeeRichards` 70 | 71 | Raises: 72 | AssertionError: unknown algorithm specified 73 | """ 74 | if alg == ShrakeRupley: 75 | self._c_param.alg = FREESASA_SHRAKE_RUPLEY 76 | elif alg == LeeRichards: 77 | self._c_param.alg = FREESASA_LEE_RICHARDS 78 | else: 79 | raise AssertionError("Algorithm '%s' is unknown" % alg) 80 | 81 | def algorithm(self): 82 | """ 83 | Get algorithm. 84 | 85 | Returns: 86 | str: Name of algorithm 87 | """ 88 | if self._c_param.alg == FREESASA_SHRAKE_RUPLEY: 89 | return ShrakeRupley 90 | if self._c_param.alg == FREESASA_LEE_RICHARDS: 91 | return LeeRichards 92 | raise Exception("No algorithm specified, shouldn't be possible") 93 | 94 | def setProbeRadius(self,r): 95 | """ 96 | Set probe radius. 97 | 98 | Args: 99 | r (float): probe radius in Å (>= 0) 100 | 101 | Raises: 102 | AssertionError: r < 0 103 | """ 104 | assert(r >= 0) 105 | self._c_param.probe_radius = r 106 | 107 | def probeRadius(self): 108 | """ 109 | Get probe radius. 110 | 111 | Returns: 112 | float: Probe radius in Å 113 | """ 114 | return self._c_param.probe_radius 115 | 116 | def setNPoints(self,n): 117 | """ 118 | Set number of test points in Shrake & Rupley algorithm. 119 | 120 | Args: 121 | n (int): Number of points (> 0). 122 | 123 | Raises: 124 | AssertionError: n <= 0. 125 | """ 126 | assert(n > 0) 127 | self._c_param.shrake_rupley_n_points = n 128 | 129 | def nPoints(self): 130 | """ 131 | Get number of test points in Shrake & Rupley algorithm. 132 | 133 | Returns: 134 | int: Number of points. 135 | """ 136 | return self._c_param.shrake_rupley_n_points 137 | 138 | def setNSlices(self,n): 139 | """ 140 | Set the number of slices per atom in Lee & Richards algorithm. 141 | 142 | Args: 143 | n (int): Number of slices (> 0) 144 | 145 | Raises: 146 | AssertionError: n <= 0 147 | """ 148 | assert(n> 0) 149 | self._c_param.lee_richards_n_slices = n 150 | 151 | def nSlices(self): 152 | """ 153 | Get the number of slices per atom in Lee & Richards algorithm. 154 | 155 | Returns: 156 | int: Number of slices. 157 | """ 158 | return self._c_param.lee_richards_n_slices 159 | 160 | def setNThreads(self,n): 161 | """ 162 | Set the number of threads to use in calculations. 163 | 164 | Args: 165 | n (int): Number of points (> 0) 166 | 167 | Raises: 168 | AssertionError: n <= 0 169 | """ 170 | assert(n>0) 171 | self._c_param.n_threads = n 172 | 173 | def nThreads(self): 174 | """ 175 | Get the number of threads to use in calculations. 176 | 177 | Returns: 178 | int: Number of threads. 179 | """ 180 | return self._c_param.n_threads 181 | 182 | # not pretty, but only way I've found to pass pointers around 183 | def _get_address(self, size_t ptr2ptr): 184 | cdef freesasa_parameters **p = ptr2ptr 185 | p[0] = &self._c_param -------------------------------------------------------------------------------- /src/result.pyx: -------------------------------------------------------------------------------- 1 | from cfreesasa cimport * 2 | 3 | class ResidueArea: 4 | """ 5 | Stores absolute and relative areas for a residue 6 | 7 | Attributes: 8 | residueType (str): Type of Residue 9 | residueNumber (str): Residue number 10 | hasRelativeAreas (bool): False if there was noe reference area to calculate relative areas from 11 | 12 | total (float): Total SASA of residue 13 | polar (float): Polar SASA 14 | apolar (float): Apolar SASA 15 | mainChain (float): Main chain SASA 16 | sideChain (float): Side chain SASA 17 | 18 | relativeTotal (float): Relative total SASA 19 | relativePolar (float): Relative polar SASA 20 | relativeApolar (float): Relative Apolar SASA 21 | relativeMainChain (float): Relative main chain SASA 22 | relativeSideChain (float): Relative side chain SASA 23 | """ 24 | 25 | residueType = "" 26 | residueNumber = "" 27 | hasRelativeAreas = False 28 | 29 | total = 0 30 | polar = 0 31 | apolar = 0 32 | mainChain = 0 33 | sideChain = 0 34 | 35 | relativeTotal = 0 36 | relativePolar = 0 37 | relativeApolar = 0 38 | relativeMainChain = 0 39 | relativeSideChain = 0 40 | 41 | 42 | cdef class Result: 43 | """ 44 | Stores results from SASA calculation. 45 | 46 | The type of object returned by :py:func:`freesasa.calc()`, 47 | not intended to be used outside of that context. 48 | """ 49 | 50 | cdef freesasa_result* _c_result 51 | cdef freesasa_node* _c_root_node 52 | 53 | ## The constructor 54 | def __init__ (self): 55 | self._c_result = NULL 56 | self._c_root_node = NULL 57 | 58 | ## The destructor 59 | def __dealloc__(self): 60 | if self._c_result is not NULL: 61 | freesasa_result_free(self._c_result) 62 | if self._c_root_node is not NULL: 63 | freesasa_node_free(self._c_root_node) 64 | 65 | def nAtoms(self): 66 | """ 67 | Number of atoms in the results. 68 | 69 | Returns: 70 | int: Number of atoms. 71 | """ 72 | if self._c_result is not NULL: 73 | return self._c_result.n_atoms 74 | return 0 75 | 76 | def totalArea(self): 77 | """ 78 | Total SASA. 79 | 80 | Returns: 81 | The total area in Å^2. 82 | Raises: 83 | AssertionError: If no results have been associated with the object. 84 | """ 85 | assert(self._c_result is not NULL) 86 | return self._c_result.total 87 | 88 | def atomArea(self,i): 89 | """ 90 | SASA for a given atom. 91 | 92 | Args: 93 | i (int): index of atom. 94 | 95 | Returns: 96 | float: SASA of atom i in Å^2. 97 | 98 | Raise: 99 | AssertionError: If no results have been associated 100 | with the object or if index is out of bounds 101 | """ 102 | assert(self._c_result is not NULL) 103 | assert(i < self._c_result.n_atoms) 104 | return self._c_result.sasa[i] 105 | 106 | def residueAreas(self): 107 | """ 108 | Get SASA for all residues including relative areas if available for the 109 | classifier used. 110 | 111 | Returns dictionary of results where first dimension is chain label and 112 | the second dimension residue number. I.e. ``result["A"]["5"]`` gives the 113 | :py:class:`.ResidueArea` of residue number 5 in chain A. 114 | 115 | Relative areas are normalized to 1, but can be > 1 for 116 | residues in unusual conformations or at the ends of chains. 117 | 118 | Returns: 119 | dictionary 120 | 121 | Raise: 122 | AssertionError: If no results or structure has been associated 123 | with the object. 124 | """ 125 | assert(self._c_result is not NULL) 126 | assert(self._c_root_node is not NULL, "Result.residueAreas can only be called on results generated directly or indirectly by freesasa.calc()") 127 | 128 | cdef freesasa_node* result_node = freesasa_node_children(self._c_root_node) 129 | cdef freesasa_node* structure = freesasa_node_children(result_node) 130 | cdef freesasa_node* chain 131 | cdef freesasa_node* residue 132 | cdef freesasa_nodearea* c_area 133 | cdef freesasa_nodearea* c_ref_area 134 | 135 | result = {} 136 | 137 | chain = freesasa_node_children(structure) 138 | while (chain != NULL): 139 | residue = freesasa_node_children(chain) 140 | chainLabel = freesasa_node_name(chain) 141 | result[chainLabel] = {} 142 | 143 | while (residue != NULL): 144 | c_area = freesasa_node_area(residue) 145 | c_ref_area = freesasa_node_residue_reference(residue) 146 | residueNumber = freesasa_node_residue_number(residue).strip() 147 | residueType = freesasa_node_name(residue).strip() 148 | 149 | area = ResidueArea() 150 | 151 | area.residueType = residueType 152 | area.residueNumber = residueNumber 153 | 154 | area.total = c_area.total 155 | area.mainChain = c_area.main_chain 156 | area.sideChain = c_area.side_chain 157 | area.polar = c_area.polar 158 | area.apolar = c_area.apolar 159 | 160 | if (c_ref_area is not NULL): 161 | area.hasRelativeAreas = True 162 | area.relativeTotal = self._safe_div(c_area.total, c_ref_area.total) 163 | area.relativeMainChain = self._safe_div(c_area.main_chain, c_ref_area.main_chain) 164 | area.relativeSideChain = self._safe_div(c_area.side_chain, c_ref_area.side_chain) 165 | area.relativePolar = self._safe_div(c_area.polar, c_ref_area.polar) 166 | area.relativeApolar = self._safe_div(c_area.apolar, c_ref_area.apolar) 167 | 168 | result[chainLabel][residueNumber] = area 169 | 170 | residue = freesasa_node_next(residue) 171 | 172 | chain = freesasa_node_next(chain) 173 | 174 | return result 175 | 176 | def write_pdb(self, filename): 177 | if self._c_root_node is NULL: 178 | raise AssertionError('Result root node points to NULL. Unable to write to a pdb file.') 179 | 180 | cdef freesasa_node *result_node = freesasa_node_children(self._c_root_node) 181 | cdef freesasa_node *structure_node = freesasa_node_children(result_node) 182 | cdef freesasa_node *chain_node = freesasa_node_children(structure_node) 183 | cdef freesasa_node *residue_node = freesasa_node_children(chain_node) 184 | cdef freesasa_node *atom_node = freesasa_node_children(residue_node) 185 | 186 | cdef const char * atom_pdb_line = freesasa_node_atom_pdb_line(atom_node) 187 | 188 | if atom_pdb_line is NULL: 189 | raise AssertionError( 190 | "Atom PDB Line is NULL. You are probably trying to write a PDB file from a Bio.Structure." 191 | ) 192 | 193 | cdef FILE *f = NULL 194 | 195 | f = fopen(filename, 'w') 196 | freesasa_write_pdb(f, self._c_root_node) 197 | fclose(f) 198 | 199 | def _safe_div(self,a,b): 200 | try: 201 | return a/b 202 | except ZeroDivisionError: 203 | return float('nan') 204 | 205 | def _get_address(self, size_t ptr2ptr): 206 | cdef freesasa_result **p = ptr2ptr 207 | p[0] = self._c_result 208 | -------------------------------------------------------------------------------- /src/structure.pyx: -------------------------------------------------------------------------------- 1 | import string 2 | import warnings 3 | from cfreesasa cimport * 4 | from libc.stdio cimport FILE, fopen, fclose 5 | from libc.stdlib cimport malloc, realloc 6 | 7 | cdef class Structure: 8 | """ 9 | Represents a protein structure, including its atomic radii. 10 | 11 | Initialized from PDB-file. Calculates atomic radii using default 12 | classifier, or custom one provided as argument to initalizer. 13 | 14 | Default options are :: 15 | 16 | Structure.defaultOptions = { 17 | 'hetatm' : False, # False: skip HETATM 18 | # True: include HETATM 19 | 20 | 'hydrogen' : False, # False: ignore hydrogens 21 | # True: include hydrogens 22 | 23 | 'join-models' : False, # False: Only use the first MODEL 24 | # True: Include all MODELs 25 | 26 | 'skip-unknown' : False, # False: Guess radius for unknown atoms 27 | # based on element 28 | # True: Skip unknown atoms 29 | 30 | 'halt-at-unknown' : False # False: set radius for unknown atoms, 31 | # that can not be guessed to 0. 32 | # True: Throw exception on unknown atoms. 33 | } 34 | 35 | Attributes: 36 | defaultOptions: Default options for reading structure from PDB. 37 | 38 | """ 39 | cdef freesasa_structure* _c_structure 40 | cdef const freesasa_classifier* _c_classifier 41 | cdef int _c_options 42 | 43 | defaultOptions = { 44 | 'hetatm' : False, 45 | 'hydrogen' : False, 46 | 'join-models' : False, 47 | 'skip-unknown' : False, 48 | 'halt-at-unknown' : False 49 | } 50 | 51 | defaultStructureArrayOptions = { 52 | 'hetatm' : False, 53 | 'hydrogen' : False, 54 | 'separate-chains' : True, 55 | 'separate-models' : False 56 | } 57 | 58 | def __init__(self, fileName=None, classifier=None, 59 | options = defaultOptions): 60 | """ 61 | Constructor 62 | 63 | If a PDB file is provided, the structure will be constructed 64 | based on the file. If not, this simply initializes an empty 65 | structure with the given classifier and options. Atoms will then 66 | have to be added manually using `:py:meth:`.Structure.addAtom()`. 67 | 68 | Args: 69 | fileName (str): PDB file (if `None` empty structure generated). 70 | classifier: An optional :py:class:`.Classifier` to calculate atomic 71 | radii, uses default if none provided. 72 | This classifier will also be used in calls to :py:meth:`.Structure.addAtom()` 73 | but only if it's the default classifier, one of the standard 74 | classifiers from :py:meth:`.Classifier.getStandardClassifier()`, 75 | or defined by a config-file (i.e. if it uses the underlying 76 | C API). 77 | options (dict): specify which atoms and models to include, default is 78 | :py:attr:`.Structure.defaultOptions` 79 | 80 | Raises: 81 | IOError: Problem opening/reading file. 82 | Exception: Problem parsing PDB file or calculating 83 | atomic radii. 84 | Exception: If option 'halt-at-unknown' selected and 85 | unknown atom encountered. 86 | """ 87 | 88 | self._c_structure = NULL 89 | self._c_classifier = NULL 90 | 91 | if classifier is None: 92 | classifier = Classifier() 93 | if classifier._isCClassifier(): 94 | classifier._get_address(&self._c_classifier) 95 | 96 | Structure._validate_options(options) 97 | self._c_options = Structure._get_bitfield_from_options(options) 98 | 99 | if fileName is None: 100 | self._c_structure = freesasa_structure_new() 101 | else: 102 | self._initFromFile(fileName, classifier) 103 | 104 | def _initFromFile(self, fileName, classifier): 105 | cdef FILE *input 106 | input = fopen(fileName,'rb') 107 | if input is NULL: 108 | raise IOError("File '%s' could not be opened." % fileName) 109 | 110 | if not classifier._isCClassifier(): # supress warnings 111 | setVerbosity(silent) 112 | 113 | self._c_structure = freesasa_structure_from_pdb(input, self._c_classifier, self._c_options) 114 | 115 | if not classifier._isCClassifier(): 116 | setVerbosity(normal) 117 | 118 | fclose(input) 119 | 120 | if self._c_structure is NULL: 121 | raise Exception("Error reading '%s'." % fileName) 122 | 123 | # for pure Python classifiers we use the default 124 | # classifier above to initialize the structure and then 125 | # reassign radii using the provided classifier here 126 | if (not classifier._isCClassifier()): 127 | self.setRadiiWithClassifier(classifier) 128 | 129 | 130 | def addAtom(self, atomName, residueName, residueNumber, chainLabel, x, y, z): 131 | """ 132 | Add atom to structure. 133 | 134 | This function is meant to be used if the structure was not 135 | initialized from a PDB. The options and classifier passed to 136 | the constructor for the :py:class:`.Structure` will be used 137 | (see the documentation of the constructor for restrictions). 138 | The radii set by the classifier can be overriden by calling 139 | :py:meth:`.Structure.setRadiiWithClassifier()` afterwards. 140 | 141 | There are no restraints on string lengths for the arguments, but 142 | the atom won't be added if the classifier doesn't 143 | recognize the atom and also cannot deduce its element from the 144 | atom name. 145 | 146 | Args: 147 | atomName (str): atom name (e.g. `"CA"`) 148 | residueName (str): residue name (e.g. `"ALA"`) 149 | residueNumber (str or int): residue number (e.g. `'12'`) 150 | or integer. Some PDBs have residue-numbers that aren't 151 | regular numbers. Therefore treated as a string primarily. 152 | chainLabel (str): 1-character string with chain label (e.g. 'A') 153 | x,y,z (float): coordinates 154 | 155 | Raises: 156 | Exception: Residue-number invalid 157 | AssertionError: 158 | """ 159 | if (type(residueNumber) is str): 160 | resnum = residueNumber 161 | elif (type(residueNumber) is int): 162 | resnum = "%d" % residueNumber 163 | else: 164 | raise Exception("Residue-number invalid, must be either string or number") 165 | 166 | cdef const char *label = chainLabel 167 | 168 | ret = freesasa_structure_add_atom_wopt( 169 | self._c_structure, atomName, 170 | residueName, resnum, label[0], 171 | x, y, z, 172 | self._c_classifier, self._c_options) 173 | 174 | assert(ret != FREESASA_FAIL) 175 | 176 | def addAtoms(self, atomNames, residueNames, residueNumbers, chainLabels, xs, ys, zs): 177 | """ 178 | Add multiple atoms to structure. 179 | 180 | Args: 181 | atomNames (list): list of atom name (e.g. `["CA"]`) 182 | residueNames (list): list of residue name (e.g. `["ALA"]`) 183 | residueNumbers (list): list of residue number (e.g. `['12']`) 184 | or integer. Some PDBs have residue-numbers that aren't 185 | regular numbers. Therefore treated as a string primarily. 186 | chainLabels (list): list of 1-character string with chain label (e.g. ['A']) 187 | xs,ys,zs (list): list of coordinates 188 | 189 | Raises: 190 | AssertionError: inconsistent size of input args 191 | """ 192 | assert(len(set([len(atomNames), len(residueNames), len(residueNumbers), \ 193 | len(chainLabels), len(xs), len(ys), len(zs)])) == 1), "Inconsistent size of input args" 194 | 195 | for i in range(len(atomNames)): 196 | self.addAtom(atomNames[i], residueNames[i], residueNumbers[i], \ 197 | chainLabels[i], xs[i], ys[i], zs[i]) 198 | 199 | def setRadiiWithClassifier(self,classifier): 200 | """ 201 | Assign radii to atoms in structure using a classifier. 202 | 203 | Args: 204 | classifier: A :py:class:`.Classifier` to use to calculate radii. 205 | 206 | Raises: 207 | AssertionError: if structure not properly initialized 208 | """ 209 | assert(self._c_structure is not NULL) 210 | n = self.nAtoms() 211 | r = [] 212 | for i in range(0,n): 213 | r.append(classifier.radius(self.residueName(i), self.atomName(i))) 214 | self.setRadii(r) 215 | 216 | def setRadii(self,radiusArray): 217 | """ 218 | Set atomic radii from an array 219 | 220 | Args: 221 | radiusArray (list): Array of atomic radii in Ångström, should 222 | have nAtoms() elements. 223 | Raises: 224 | AssertionError: if radiusArray has wrong dimension, structure 225 | not properly initialized, or if the array contains 226 | negative radii (not properly classified?) 227 | """ 228 | assert(self._c_structure is not NULL) 229 | n = self.nAtoms() 230 | assert len(radiusArray) == n 231 | cdef double *r = malloc(sizeof(double)*n) 232 | assert(r is not NULL) 233 | for i in range(0,n): 234 | r[i] = radiusArray[i] 235 | assert(r[i] >= 0), "Error: Radius is <= 0 (" + str(r[i]) + ") for the residue: " + self.residueName(i) + ", atom: " + self.atomName(i) 236 | freesasa_structure_set_radius(self._c_structure, r) 237 | 238 | def nAtoms(self): 239 | """ 240 | Number of atoms. 241 | 242 | Returns: 243 | int: Number of atoms 244 | 245 | Raises: 246 | AssertionError: if not properly initialized 247 | """ 248 | assert(self._c_structure is not NULL) 249 | return freesasa_structure_n(self._c_structure) 250 | 251 | def radius(self,i): 252 | """ 253 | Radius of atom. 254 | 255 | Args: 256 | i (int): Index of atom. 257 | 258 | Returns: 259 | float: Radius in Å. 260 | 261 | Raises: 262 | AssertionError: if index out of bounds, object not properly initalized. 263 | """ 264 | assert(i >= 0 and i < self.nAtoms()) 265 | assert(self._c_structure is not NULL) 266 | cdef const double *r = freesasa_structure_radius(self._c_structure) 267 | assert(r is not NULL) 268 | return r[i] 269 | 270 | def setRadius(self, atomIndex, radius): 271 | """ 272 | Set radius for a given atom 273 | 274 | Args: 275 | atomIndex (int): Index of atom 276 | radius (float): Value of radius 277 | 278 | Raises: 279 | AssertionError: if index out of bounds, radius 280 | negative, or structure not properly initialized 281 | """ 282 | assert(self._c_structure is not NULL) 283 | assert(atomIndex >= 0 and atomIndex < self.nAtoms()) 284 | assert(radius >= 0) 285 | freesasa_structure_atom_set_radius(self._c_structure, atomIndex, radius) 286 | 287 | def atomName(self,i): 288 | """ 289 | Get atom name 290 | 291 | Args: 292 | i (int): Atom index. 293 | 294 | Returns: 295 | str: Atom name as 4-character string. 296 | 297 | Raises: 298 | AssertionError: if index out of range or Structure not properly initialized. 299 | """ 300 | assert(i >= 0 and i < self.nAtoms()) 301 | assert(self._c_structure is not NULL) 302 | return freesasa_structure_atom_name(self._c_structure,i) 303 | 304 | def residueName(self,i): 305 | """ 306 | Get residue name of given atom. 307 | 308 | Args: 309 | i (int): Atom index. 310 | 311 | Returns: 312 | str: Residue name as 3-character string. 313 | 314 | Raises: 315 | AssertionError: if index out of range or Structure not properly initialized 316 | """ 317 | assert(i >= 0 and i < self.nAtoms()) 318 | assert(self._c_structure is not NULL) 319 | return freesasa_structure_atom_res_name(self._c_structure,i) 320 | 321 | def residueNumber(self,i): 322 | """ 323 | Get residue number for given atom. 324 | 325 | Residue number will include the insertion code if there is one. 326 | 327 | Args: 328 | i (int): Atom index. 329 | 330 | Returns: 331 | str: Residue number as 5-character string (last character is either whitespace or insertion code) 332 | 333 | Raises: 334 | AssertionError: if index out of range or Structure not properly initialized 335 | """ 336 | assert(i >= 0 and i < self.nAtoms()) 337 | assert(self._c_structure is not NULL) 338 | return freesasa_structure_atom_res_number(self._c_structure,i) 339 | 340 | def chainLabel(self,i): 341 | """ 342 | Get chain label for given atom. 343 | 344 | Args: 345 | i (int): Atom index. 346 | 347 | Returns: 348 | str: Chain label as 1-character string. 349 | 350 | Raises: 351 | AssertionError: if index out of range or Structure not properly initialized 352 | """ 353 | assert(i >= 0 and i < self.nAtoms()) 354 | assert(self._c_structure is not NULL) 355 | cdef char label[2] 356 | label[0] = freesasa_structure_atom_chain(self._c_structure,i) 357 | label[1] = '\0' 358 | return label 359 | 360 | def coord(self, i): 361 | """ 362 | Get coordinates of given atom. 363 | 364 | Args: 365 | i (int): Atom index. 366 | 367 | Returns: 368 | list: array of x, y, and z coordinates 369 | 370 | Raises: 371 | AssertionError: if index out of range or Structure not properly initialized 372 | """ 373 | assert(i >= 0 and i < self.nAtoms()) 374 | assert(self._c_structure is not NULL) 375 | cdef const double *coord = freesasa_structure_coord_array(self._c_structure) 376 | return [coord[3*i], coord[3*i+1], coord[3*i+2]] 377 | 378 | @staticmethod 379 | def _validate_options(param): 380 | # check validity of options 381 | knownOptions = {'hetatm','hydrogen','join-models','separate-models', 382 | 'separate-chains', 'chain-groups', 'skip-unknown', 'halt-at-unknown'} 383 | unknownOptions = set(param.keys()).difference(knownOptions) 384 | if len(unknownOptions) > 0: 385 | raise AssertionError("Option(s): ",unknownOptions," unknown.") 386 | 387 | # 'chain-groups' additional checks 388 | if param.get('chain-groups', False): 389 | if param.get('separate-chains', False): 390 | raise ValueError("'chain-groups' and 'separate-chains' cannot be set simultaneously.") 391 | allowed_chars = set(string.ascii_lowercase + string.ascii_uppercase + '+') 392 | if not all([character in allowed_chars for character in param['chain-groups']]): 393 | raise ValueError("'{}' is not a valid chain-groups selection.".format(param['chain-groups'])) 394 | 395 | @staticmethod 396 | def _get_bitfield_from_options(param): 397 | options = 0 398 | # calculate bitfield 399 | if 'hetatm' in param and param['hetatm']: 400 | options |= FREESASA_INCLUDE_HETATM 401 | if 'hydrogen' in param and param['hydrogen']: 402 | options |= FREESASA_INCLUDE_HYDROGEN 403 | if 'join-models' in param and param['join-models']: 404 | options |= FREESASA_JOIN_MODELS 405 | if 'separate-models' in param and param['separate-models']: 406 | options |= FREESASA_SEPARATE_MODELS 407 | if 'separate-chains' in param and param['separate-chains']: 408 | options |= FREESASA_SEPARATE_CHAINS 409 | if 'skip-unknown' in param and param['skip-unknown']: 410 | options |= FREESASA_SKIP_UNKNOWN 411 | if 'halt-at-unknown' in param and param['halt-at-unknown']: 412 | options |= FREESASA_HALT_AT_UNKNOWN 413 | return options 414 | 415 | def _get_address(self, size_t ptr2ptr): 416 | cdef freesasa_structure **p = ptr2ptr 417 | p[0] = self._c_structure 418 | 419 | def _set_address(self, size_t ptr2ptr): 420 | cdef freesasa_structure **p = ptr2ptr 421 | self._c_structure = p[0] 422 | 423 | ## The destructor 424 | def __dealloc__(self): 425 | if self._c_structure is not NULL: 426 | freesasa_structure_free(self._c_structure) 427 | 428 | 429 | def structureArray(fileName, 430 | options = Structure.defaultStructureArrayOptions, 431 | classifier = None): 432 | """ 433 | Create array of structures from PDB file. 434 | 435 | Split PDB file into several structures by either by treating 436 | chains separately and/or by treating each MODEL as a separate 437 | structure and/or grouping chains. 438 | 439 | Args: 440 | fileName (str): The PDB file. 441 | options (dict): Specification for how to read the PDB-file 442 | (see :py:attr:`.Structure.defaultStructureArrayOptions` for 443 | options and default value). 444 | classifier: :py:class:`.Classifier` to assign atoms radii, default is used 445 | if none specified. 446 | 447 | Returns: 448 | list: An array of :py:class:`.Structure` 449 | 450 | Raises: 451 | AssertionError: if `fileName` is None 452 | AssertionError: if an option value is not recognized 453 | AssertionError: if neither of the options `'separate-chains'` 454 | and `'separate-models'` are specified. 455 | IOError: if can't open file 456 | Exception: if there are problems parsing the input 457 | """ 458 | 459 | assert fileName is not None 460 | # we need to have at least one of these 461 | assert(('separate-chains' in options and options['separate-chains'] is True) 462 | or ('separate-models' in options and options['separate-models'] is True) 463 | or (options.get('chain-groups', False))) 464 | 465 | Structure._validate_options(options) 466 | structure_options = Structure._get_bitfield_from_options(options) 467 | cdef FILE *input 468 | input = fopen(fileName,'rb') 469 | if input is NULL: 470 | raise IOError("File '%s' could not be opened." % fileName) 471 | 472 | verbosity = getVerbosity() 473 | 474 | if classifier is not None: 475 | setVerbosity(silent) 476 | 477 | cdef int n 478 | cdef freesasa_structure** sArray 479 | if options.get('separate-chains', False) or options.get('separate-models', False): 480 | sArray = freesasa_structure_array(input,&n,NULL,structure_options) 481 | else: 482 | sArray = malloc(sizeof(freesasa_structure *)) 483 | sArray[0] = freesasa_structure_from_pdb(input, NULL, structure_options) 484 | n = 1 485 | fclose(input) 486 | 487 | if sArray is NULL: 488 | raise Exception("Problems reading structures in '%s'." % fileName) 489 | if sArray[0] is NULL: 490 | free(sArray) 491 | raise Exception("Problems reading structures in '%s'." % fileName) 492 | 493 | cdef freesasa_structure* group 494 | if options.get('chain-groups', False): 495 | # Add groups from each model 496 | chain_groups = options['chain-groups'].split('+') 497 | n_groups = len(chain_groups) 498 | n_total = n * (1 + n_groups) 499 | sArray = realloc(sArray, n_total * sizeof(freesasa_structure*)) 500 | if sArray is NULL: 501 | raise Exception("Out of memory when allocating '%i' structures from '%s'." % (n_total, fileName)) 502 | for i in range(0, n): 503 | for j, chainID_group in enumerate(chain_groups): 504 | idx = j + (i * n_groups) + n 505 | group = freesasa_structure_get_chains(sArray[i], chainID_group, NULL, structure_options) 506 | sArray[idx] = group 507 | n = n_total 508 | 509 | if classifier is not None: 510 | setVerbosity(verbosity) 511 | 512 | structures = [] 513 | for i in range(0, n): 514 | structures.append(Structure()) 515 | structures[-1]._set_address( &sArray[i]) 516 | if classifier is not None: 517 | structures[-1].setRadiiWithClassifier(classifier) 518 | free(sArray) 519 | 520 | return structures 521 | 522 | 523 | def structureFromBioPDB(bioPDBStructure, classifier=None, options = Structure.defaultOptions): 524 | """ 525 | Create a freesasa structure from a Bio.PDB structure 526 | 527 | Experimental, not thorougly tested yet. 528 | Structures generated this way will not preserve whitespace in residue numbers, etc, 529 | as in :py:class:`.Structure`. 530 | 531 | Args: 532 | bioPDBStructure: a `Bio.PDB` structure 533 | classifier: an optional :py:class:`.Classifier` to specify atomic radii 534 | options (dict): Options supported are `'hetatm'`, `'skip-unknown'` and `'halt-at-unknown'` 535 | 536 | Returns: 537 | :py:class:`.Structure`: The structure 538 | 539 | Raises: 540 | Exception: if option 'halt-at-unknown' is selected and 541 | unknown atoms are encountered. Passes on exceptions from 542 | :py:meth:`.Structure.addAtom()` and 543 | :py:meth:`.Structure.setRadiiWithClassifier()`. 544 | """ 545 | structure = Structure() 546 | if (classifier is None): 547 | classifier = Classifier() 548 | Structure._validate_options(options) 549 | optbitfield = Structure._get_bitfield_from_options(options) 550 | 551 | atoms = bioPDBStructure.get_atoms() 552 | 553 | if (not optbitfield & FREESASA_JOIN_MODELS): 554 | models = bioPDBStructure.get_models() 555 | atoms = next(models).get_atoms() 556 | 557 | for a in atoms: 558 | r = a.get_parent() 559 | s = r.get_parent() 560 | hetflag, resseq, icode = r.get_id() 561 | resname = r.get_resname() 562 | atomname = a.get_fullname() 563 | element = a.element 564 | 565 | if (hetflag is not ' ' and not (optbitfield & FREESASA_INCLUDE_HETATM)): 566 | continue 567 | if (((element == "H") or (element == "D")) and not (optbitfield & FREESASA_INCLUDE_HYDROGEN)): 568 | continue 569 | 570 | c = r.get_parent() 571 | v = a.get_vector() 572 | if (icode): 573 | resseq = str(resseq) + str(icode) 574 | 575 | if (classifier.classify(resname, atomname) is 'Unknown'): 576 | if (optbitfield & FREESASA_SKIP_UNKNOWN): 577 | continue 578 | if (optbitfield & FREESASA_HALT_AT_UNKNOWN): 579 | raise Exception("Halting at unknown atom") 580 | 581 | structure.addAtom(atomname, r.get_resname(), resseq, c.get_id(), 582 | v[0], v[1], v[2]) 583 | 584 | structure.setRadiiWithClassifier(classifier) 585 | return structure 586 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | from freesasa import * 2 | import unittest 3 | import math 4 | import os 5 | import faulthandler 6 | 7 | # this class tests using derived classes to create custom Classifiers 8 | class DerivedClassifier(Classifier): 9 | purePython = True 10 | 11 | def classify(self,residueName,atomName): 12 | return 'bla' 13 | 14 | def radius(self,residueName,atomName): 15 | return 10 16 | 17 | class FreeSASATestCase(unittest.TestCase): 18 | def testParameters(self): 19 | d = Parameters.defaultParameters 20 | p = Parameters() 21 | self.assertTrue(p.algorithm() == LeeRichards) 22 | self.assertTrue(p.algorithm() == d['algorithm']) 23 | self.assertTrue(p.probeRadius() == d['probe-radius']) 24 | self.assertTrue(p.nPoints() == d['n-points']) 25 | self.assertTrue(p.nSlices() == d['n-slices']) 26 | self.assertTrue(p.nThreads() == d['n-threads']) 27 | self.assertRaises(AssertionError,lambda: Parameters({'not-an-option' : 1})) 28 | self.assertRaises(AssertionError,lambda: Parameters({'n-slices' : 50, 'not-an-option' : 1})) 29 | self.assertRaises(AssertionError,lambda: Parameters({'not-an-option' : 50, 'also-not-an-option' : 1})) 30 | 31 | p.setAlgorithm(ShrakeRupley) 32 | self.assertTrue(p.algorithm() == ShrakeRupley) 33 | p.setAlgorithm(LeeRichards) 34 | self.assertTrue(p.algorithm() == LeeRichards) 35 | self.assertRaises(AssertionError,lambda: p.setAlgorithm(-10)) 36 | 37 | p.setProbeRadius(1.5) 38 | self.assertTrue(p.probeRadius() == 1.5) 39 | self.assertRaises(AssertionError,lambda: p.setProbeRadius(-1)) 40 | 41 | p.setNPoints(20) 42 | self.assertTrue(p.nPoints() == 20) 43 | self.assertRaises(AssertionError,lambda: p.setNPoints(0)) 44 | 45 | p.setNSlices(10) 46 | self.assertTrue(p.nSlices() == 10) 47 | self.assertRaises(AssertionError,lambda: p.setNSlices(0)) 48 | 49 | p.setNThreads(2) 50 | self.assertTrue(p.nThreads() == 2) 51 | self.assertRaises(AssertionError, lambda: p.setNThreads(0)) 52 | 53 | def testResult(self): 54 | r = Result() 55 | self.assertRaises(AssertionError,lambda: r.totalArea()) 56 | self.assertRaises(AssertionError,lambda: r.atomArea(0)) 57 | 58 | def testClassifier(self): 59 | c = Classifier() 60 | self.assertTrue(c._isCClassifier()) 61 | self.assertTrue(c.classify("ALA"," CB ") == apolar) 62 | self.assertTrue(c.classify("ARG"," NH1") == polar) 63 | self.assertTrue(c.radius("ALA"," CB ") == 1.88) 64 | 65 | setVerbosity(silent) 66 | self.assertRaises(Exception,lambda: Classifier("lib/tests/data/err.config")) 67 | self.assertRaises(IOError,lambda: Classifier("")) 68 | setVerbosity(normal) 69 | 70 | c = Classifier("lib/tests/data/test.config") 71 | self.assertTrue(c.classify("AA","aa") == "Polar") 72 | self.assertTrue(c.classify("BB","bb") == "Apolar") 73 | self.assertTrue(c.radius("AA","aa") == 1.0) 74 | self.assertTrue(c.radius("BB","bb") == 2.0) 75 | 76 | c = Classifier("lib/share/oons.config") 77 | self.assertTrue(c.radius("ALA"," CB ") == 2.00) 78 | 79 | c = DerivedClassifier() 80 | self.assertTrue(not c._isCClassifier()) 81 | self.assertTrue(c.radius("ALA"," CB ") == 10) 82 | self.assertTrue(c.radius("ABCDEFG","HIJKLMNO") == 10) 83 | self.assertTrue(c.classify("ABCDEFG","HIJKLMNO") == "bla") 84 | 85 | def testStructure(self): 86 | self.assertRaises(IOError,lambda: Structure("xyz#$%")) 87 | setVerbosity(silent) 88 | # test any file that's not a PDB file 89 | self.assertRaises(Exception,lambda: Structure("lib/tests/data/err.config")) 90 | self.assertRaises(Exception,lambda: Structure("lib/tests/data/empty.pdb")) 91 | self.assertRaises(Exception,lambda: Structure("lib/tests/data/empty_model.pdb")) 92 | setVerbosity(normal) 93 | 94 | s = Structure("lib/tests/data/1ubq.pdb") 95 | self.assertTrue(s.nAtoms() == 602) 96 | self.assertTrue(s.radius(1) == 1.88) 97 | self.assertTrue(s.chainLabel(1) == 'A') 98 | self.assertTrue(s.atomName(1) == ' CA ') 99 | self.assertTrue(s.residueName(1) == 'MET') 100 | self.assertTrue(s.residueNumber(1) == ' 1 ') 101 | 102 | s2 = Structure("lib/tests/data/1ubq.pdb",Classifier("lib/share/oons.config")) 103 | self.assertTrue(s.nAtoms() == 602) 104 | self.assertTrue(math.fabs(s2.radius(1) - 2.0) < 1e-5) 105 | 106 | s2 = Structure("lib/tests/data/1ubq.pdb",Classifier("lib/share/protor.config")) 107 | for i in range (0,601): 108 | self.assertTrue(math.fabs(s.radius(i)- s2.radius(i)) < 1e-5) 109 | 110 | self.assertRaises(Exception,lambda: Structure("lib/tests/data/1ubq.pdb","lib/tests/data/err.config")) 111 | 112 | s = Structure() 113 | s.addAtom(' CA ','ALA',' 1','A',1,1,1) 114 | self.assertTrue(s.nAtoms() == 1) 115 | self.assertTrue(s.atomName(0) == ' CA ') 116 | self.assertTrue(s.residueName(0) == 'ALA') 117 | self.assertTrue(s.residueNumber(0) == ' 1') 118 | self.assertTrue(s.chainLabel(0) == 'A') 119 | self.assertTrue(s.nAtoms() == 1) 120 | x, y, z = s.coord(0) 121 | self.assertTrue(x == 1 and y == 1 and z == 1) 122 | s.addAtom(' CB ','ALA',2,'A',2,1,1) 123 | self.assertTrue(s.nAtoms() == 2) 124 | self.assertTrue(s.residueNumber(1) == '2') 125 | 126 | # reinitialize s and test addAtoms function 127 | s = Structure() 128 | s.addAtoms([' CA ',' CB '], ['ALA','ALA'],[' 1',2],['A','A'],[1,2],[1,1],[1,1]) 129 | self.assertTrue(s.nAtoms() == 2) 130 | self.assertTrue(s.residueNumber(1) == '2') 131 | 132 | self.assertRaises(AssertionError, lambda: s.atomName(3)) 133 | self.assertRaises(AssertionError, lambda: s.residueName(3)) 134 | self.assertRaises(AssertionError, lambda: s.residueNumber(3)) 135 | self.assertRaises(AssertionError, lambda: s.chainLabel(3)) 136 | self.assertRaises(AssertionError, lambda: s.coord(3)) 137 | self.assertRaises(AssertionError, lambda: s.radius(3)) 138 | 139 | s.setRadiiWithClassifier(Classifier()) 140 | self.assertTrue(s.radius(0) == 1.88) 141 | self.assertTrue(s.radius(1) == 1.88) 142 | 143 | s.setRadiiWithClassifier(DerivedClassifier()) 144 | self.assertTrue(s.radius(0) == s.radius(1) == 10.0) 145 | 146 | s.setRadii([1.0,3.0]) 147 | self.assertTrue(s.radius(0) == 1.0) 148 | self.assertTrue(s.radius(1) == 3.0) 149 | 150 | s.setRadius(0, 10.0) 151 | self.assertTrue(s.radius(0) == 10.0); 152 | 153 | self.assertRaises(AssertionError,lambda: s.setRadius(2,10)); 154 | self.assertRaises(AssertionError,lambda: s.setRadii([1])) 155 | self.assertRaises(AssertionError,lambda: s.setRadii([1,2,3])) 156 | 157 | self.assertRaises(AssertionError,lambda: s.atomName(2)) 158 | self.assertRaises(AssertionError,lambda: s.residueName(2)) 159 | self.assertRaises(AssertionError,lambda: s.residueNumber(2)) 160 | self.assertRaises(AssertionError,lambda: s.chainLabel(2)) 161 | 162 | setVerbosity(nowarnings) 163 | s = Structure("lib/tests/data/1d3z.pdb",None,{'hydrogen' : True}) 164 | self.assertTrue(s.nAtoms() == 1231) 165 | 166 | s = Structure("lib/tests/data/1d3z.pdb",None,{'hydrogen' : True, 'join-models' : True}) 167 | self.assertTrue(s.nAtoms() == 12310) 168 | 169 | s = Structure("lib/tests/data/1ubq.pdb",None,{'hetatm' : True}) 170 | self.assertTrue(s.nAtoms() == 660) 171 | 172 | s = Structure("lib/tests/data/1d3z.pdb",None,{'hydrogen' : True, 'skip-unknown' : True}) 173 | self.assertTrue(s.nAtoms() == 602) 174 | 175 | setVerbosity(silent) 176 | self.assertRaises(Exception, lambda : Structure("lib/tests/data/1d3z.pdb", None, {'hydrogen' : True, 'halt-at-unknown' : True})) 177 | setVerbosity(normal) 178 | 179 | s = Structure(options = { 'halt-at-unknown': True }) 180 | setVerbosity(silent) 181 | self.assertRaises(Exception, lambda: s.addAtom(' XX ','ALA',' 1','A',1,1,1)) 182 | setVerbosity(normal) 183 | 184 | s = Structure(options = { 'skip-unknown': True }) 185 | setVerbosity(silent) 186 | s.addAtom(' XX ','ALA',' 1','A',1,1,1) 187 | self.assertEqual(s.nAtoms(), 0) 188 | setVerbosity(normal) 189 | 190 | s = Structure(classifier = Classifier.getStandardClassifier("naccess")) 191 | s.addAtom(' CA ', 'ALA',' 1','A',1,1,1) 192 | self.assertEqual(s.radius(0), 1.87) 193 | 194 | 195 | def testStructureArray(self): 196 | # default separates chains, only uses first model (129 atoms per chain) 197 | ss = structureArray("lib/tests/data/2jo4.pdb", {"separate-chains": False, 198 | "chain-groups": "AD+B"}) 199 | self.assertTrue(len(ss) == 3) 200 | 201 | self.assertTrue(ss[0].nAtoms() == 129 * 4) 202 | self.assertTrue(ss[1].nAtoms() == 129 * 2) 203 | self.assertTrue(ss[2].nAtoms() == 129) 204 | 205 | # include all models, separate chains and include hydrogens and hetatms (286 atoms per chain) 206 | setVerbosity(nowarnings) 207 | 208 | ss = structureArray("lib/tests/data/2jo4.pdb",{'separate-models' : True, 209 | 'hydrogen' : True, 210 | 'hetatm' : True}) 211 | self.assertTrue(len(ss) == 10) 212 | for s in ss: 213 | self.assertTrue(s.nAtoms() == 286*4) 214 | 215 | # include all models, separate in groups and include hydrogens and hetatms (286 atoms per chain) 216 | ss = structureArray("lib/tests/data/2jo4.pdb",{'separate-models' : True, 217 | 'hydrogen' : True, 218 | 'hetatm' : True, 219 | 'separate-chains' : False, 220 | "chain-groups": "AD+C"}) 221 | self.assertTrue(len(ss) == 2*10+10) 222 | for i in range(10): 223 | self.assertTrue(ss[i].nAtoms() == 286*4) 224 | for i in range(10, 30, 2): 225 | self.assertTrue(ss[i].nAtoms() == 286*2) 226 | for i in range(11, 30, 2): 227 | self.assertTrue(ss[i].nAtoms() == 286) 228 | 229 | setVerbosity(normal) 230 | # check that the structures initialized this way can be used for calculations 231 | ss = structureArray("lib/tests/data/1ubq.pdb") 232 | self.assertTrue(len(ss) == 1) 233 | self.assertTrue(ss[0].nAtoms() == 602) 234 | result = calc(ss[0],Parameters({'algorithm' : ShrakeRupley})) 235 | self.assertTrue(math.fabs(result.totalArea() - 4834.716265) < 1e-5) 236 | 237 | # Test exceptions 238 | setVerbosity(silent) 239 | self.assertRaises(AssertionError,lambda: structureArray(None)) 240 | self.assertRaises(IOError,lambda: structureArray("")) 241 | self.assertRaises(Exception,lambda: structureArray("lib/tests/data/err.config")) 242 | self.assertRaises(AssertionError,lambda: structureArray("lib/tests/data/2jo4.pdb",{'not-an-option' : True})) 243 | self.assertRaises(AssertionError, 244 | lambda: structureArray("lib/tests/data/2jo4.pdb", 245 | {'not-an-option' : True, 'hydrogen' : True})) 246 | self.assertRaises(AssertionError, 247 | lambda: structureArray("lib/tests/data/2jo4.pdb", 248 | {'hydrogen' : True})) 249 | setVerbosity(normal) 250 | 251 | def testCalc(self): 252 | # test default settings 253 | structure = Structure("lib/tests/data/1ubq.pdb") 254 | result = calc(structure,Parameters({'algorithm' : ShrakeRupley})) 255 | self.assertTrue(math.fabs(result.totalArea() - 4834.716265) < 1e-5) 256 | sasa_classes = classifyResults(result,structure) 257 | self.assertTrue(math.fabs(sasa_classes['Polar'] - 2515.821238) < 1e-5) 258 | self.assertTrue(math.fabs(sasa_classes['Apolar'] - 2318.895027) < 1e-5) 259 | 260 | # test residue areas 261 | residueAreas = result.residueAreas() 262 | a76 = residueAreas['A']['76'] 263 | self.assertEqual(a76.residueType, "GLY") 264 | self.assertEqual(a76.residueNumber, "76") 265 | self.assertTrue(a76.hasRelativeAreas) 266 | self.assertTrue(math.fabs(a76.total - 142.1967898) < 1e-5) 267 | self.assertTrue(math.fabs(a76.mainChain - 142.1967898) < 1e-5) 268 | self.assertTrue(math.fabs(a76.sideChain - 0) < 1e-5) 269 | self.assertTrue(math.fabs(a76.polar - 97.297889) < 1e-5) 270 | self.assertTrue(math.fabs(a76.apolar - 44.898900) < 1e-5) 271 | self.assertTrue(math.fabs(a76.relativeTotal - 1.75357) < 1e-4) 272 | self.assertTrue(math.fabs(a76.relativeMainChain - 1.75357) < 1e-4) 273 | self.assertTrue(math.isnan(a76.relativeSideChain)) 274 | self.assertTrue(math.fabs(a76.relativePolar - 2.17912) < 1e-4) 275 | self.assertTrue(math.fabs(a76.relativeApolar - 1.23213) < 1e-4) 276 | 277 | # test L&R 278 | result = calc(structure,Parameters({'algorithm' : LeeRichards, 'n-slices' : 20})) 279 | sasa_classes = classifyResults(result,structure) 280 | self.assertTrue(math.fabs(result.totalArea() - 4804.055641) < 1e-5) 281 | self.assertTrue(math.fabs(sasa_classes['Polar'] - 2504.217302) < 1e-5) 282 | self.assertTrue(math.fabs(sasa_classes['Apolar'] - 2299.838339) < 1e-5) 283 | 284 | # test extending Classifier with derived class 285 | sasa_classes = classifyResults(result,structure,DerivedClassifier()) 286 | self.assertTrue(math.fabs(sasa_classes['bla'] - 4804.055641) < 1e-5) 287 | 288 | ## test calculating with user-defined classifier ## 289 | classifier = Classifier("lib/share/oons.config") 290 | # classifier passed to assign user-defined radii, could also have used setRadiiWithClassifier() 291 | structure = Structure("lib/tests/data/1ubq.pdb",classifier) 292 | result = calc(structure,Parameters({'algorithm' : ShrakeRupley})) 293 | self.assertTrue(math.fabs(result.totalArea() - 4779.5109924) < 1e-5) 294 | sasa_classes = classifyResults(result,structure,classifier) # classifier passed to get user-classes 295 | self.assertTrue(math.fabs(sasa_classes['Polar'] - 2236.9298941) < 1e-5) 296 | self.assertTrue(math.fabs(sasa_classes['Apolar'] - 2542.5810983) < 1e-5) 297 | 298 | 299 | def testCalcCoord(self): 300 | # one unit sphere 301 | radii = [1] 302 | coord = [0,0,0] 303 | parameters = Parameters() 304 | parameters.setNSlices(5000) 305 | parameters.setProbeRadius(0) 306 | parameters.setNThreads(1) 307 | result = calcCoord(coord, radii, parameters) 308 | self.assertTrue(math.fabs(result.totalArea() - 4*math.pi) < 1e-3) 309 | 310 | # two separate unit spheres 311 | radii = [1,1] 312 | coord = [0,0,0, 4,4,4] 313 | result = calcCoord(coord, radii, parameters) 314 | self.assertTrue(math.fabs(result.totalArea() - 2*4*math.pi) < 1e-3) 315 | 316 | self.assertRaises(AssertionError, 317 | lambda: calcCoord(radii, radii)) 318 | 319 | def testSelectArea(self): 320 | structure = Structure("lib/tests/data/1ubq.pdb") 321 | result = calc(structure,Parameters({'algorithm' : ShrakeRupley})) 322 | # will only test that this gets through to the C interface, 323 | # extensive checking of the parser is done in the C unit tests 324 | selections = selectArea(('s1, resn ala','s2, resi 1'),structure,result) 325 | self.assertTrue(math.fabs(selections['s1'] - 118.35) < 0.1) 326 | self.assertTrue(math.fabs(selections['s2'] - 50.77) < 0.1) 327 | 328 | def testBioPDB(self): 329 | try: 330 | from Bio.PDB import PDBParser 331 | except ImportError: 332 | print("Can't import Bio.PDB, tests skipped") 333 | pass 334 | else: 335 | parser = PDBParser(QUIET=True) 336 | bp_structure = parser.get_structure("29G11","lib/tests/data/1a0q.pdb") 337 | s1 = structureFromBioPDB(bp_structure) 338 | s2 = Structure("lib/tests/data/1a0q.pdb") 339 | self.assertTrue(s1.nAtoms() == s2.nAtoms()) 340 | 341 | # make sure we got the insertion code 342 | self.assertEqual(s1.residueNumber(2286), '82A') 343 | 344 | for i in range(0, s2.nAtoms()): 345 | self.assertTrue(s1.radius(i) == s2.radius(i)) 346 | 347 | # there can be tiny errors here 348 | self.assertTrue(math.fabs(s1.coord(i)[0] - s2.coord(i)[0]) < 1e-5) 349 | self.assertTrue(math.fabs(s1.coord(i)[1] - s2.coord(i)[1]) < 1e-5) 350 | self.assertTrue(math.fabs(s1.coord(i)[2] - s2.coord(i)[2]) < 1e-5) 351 | 352 | # whitespace won't match 353 | self.assertIn(s1.residueNumber(i), s2.residueNumber(i)) 354 | 355 | # because Bio.PDB structures will have slightly different 356 | # coordinates (due to rounding errors) we set the 357 | # tolerance as high as 1e-3 358 | result = calc(s1, Parameters({'algorithm' : LeeRichards, 'n-slices' : 20})) 359 | self.assertTrue(math.fabs(result.totalArea() - 18923.280586) < 1e-3) 360 | sasa_classes = classifyResults(result, s1) 361 | self.assertTrue(math.fabs(sasa_classes['Polar'] - 9143.066411) < 1e-3) 362 | self.assertTrue(math.fabs(sasa_classes['Apolar'] - 9780.2141746) < 1e-3) 363 | residue_areas = result.residueAreas() 364 | self.assertTrue(math.fabs(residue_areas['L']['2'].total - 43.714) < 1e-2) 365 | 366 | faulthandler.enable() 367 | result, sasa_classes = calcBioPDB(bp_structure, Parameters({'algorithm' : LeeRichards, 'n-slices' : 20})) 368 | self.assertTrue(math.fabs(result.totalArea() - 18923.280586) < 1e-3) 369 | self.assertTrue(math.fabs(sasa_classes['Polar'] - 9143.066411) < 1e-3) 370 | self.assertTrue(math.fabs(sasa_classes['Apolar'] - 9780.2141746) < 1e-3) 371 | residue_areas = result.residueAreas() 372 | self.assertTrue(math.fabs(residue_areas['L']['2'].total - 43.714) < 1e-2) 373 | 374 | options = {'hetatm': False, 375 | 'hydrogen': False, 376 | 'join-models': False, 377 | 'skip-unknown': True, 378 | 'halt-at-unknown': False} 379 | classifier = Classifier() 380 | 381 | bp_structure = parser.get_structure("PDB_WITH_HYDROGENS","lib/tests/data/1d3z.pdb") 382 | fs_structure = Structure("lib/tests/data/1d3z.pdb", classifier, options) 383 | 384 | fsfrombp = structureFromBioPDB(bp_structure, classifier, options) 385 | self.assertEqual(fs_structure.nAtoms(), fsfrombp.nAtoms()) 386 | 387 | if __name__ == '__main__': 388 | # make sure we're in the right directory (if script is called from 389 | # outside the directory) 390 | abspath = os.path.abspath(__file__) 391 | dirname = os.path.dirname(abspath) 392 | os.chdir(dirname) 393 | unittest.main() 394 | --------------------------------------------------------------------------------