diff --git a/.gitignore b/.gitignore index c0a8e104..e2026802 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ *.log *.pyc .idea/* +venv coverage tests_dataset/*/ cmake-build-* diff --git a/build_docs.sh b/build_docs.sh index 2799ca6d..ed56f3eb 100755 --- a/build_docs.sh +++ b/build_docs.sh @@ -1,5 +1,6 @@ #!/bin/bash python -m pip install -r docs/requirements.txt +export TEST_DATA_PATH=$PWD/tests_dataset make -C ./docs html touch ./docs/build/html/.nojekyll diff --git a/docs/.gitignore b/docs/.gitignore index 378eac25..eabe709c 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -1 +1,3 @@ build +auto_examples +*.pdb \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt index 4fb6e514..303a719d 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,3 +1,8 @@ sphinx sphinx-autodoc-annotation sphinx-rtd-theme +sphinx-gallery +matplotlib +Pillow +tabulate +biopython diff --git a/docs/source/conf.py b/docs/source/conf.py index 62136ae0..2707cae0 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -42,6 +42,7 @@ 'sphinx.ext.mathjax', 'sphinx.ext.autodoc', 'sphinx_autodoc_annotation', + 'sphinx_gallery.gen_gallery', ] autosummary_generate = True @@ -318,7 +319,7 @@ def process_docs(app, objtype, fullname, object, options, docstringlines): def skip(app, what, name, obj, skip, options): - skip_list = [ "__module__","__weakref__"] + skip_list = [ "__module__", "__weakref__"] if name in skip_list: return True return skip @@ -328,3 +329,19 @@ def setup(app): app.connect('autodoc-process-signature', strip_argumet_types) app.connect('autodoc-process-docstring', process_docs) app.connect("autodoc-skip-member", skip) + +import os + +sphinx_gallery_conf = { + # path to your examples scripts + 'examples_dirs': './examples', + # path where to save gallery generated examples + 'gallery_dirs': 'auto_examples', + 'filename_pattern': '/', + 'default_thumb_file': os.path.abspath('images/python-logo.png'), + 'line_numbers': True, + + 'expected_failing_examples': [ + 'examples/PDB/read_pdb_errors.py', + ] +} diff --git a/docs/source/examples/PDB/README.txt b/docs/source/examples/PDB/README.txt new file mode 100644 index 00000000..19e76166 --- /dev/null +++ b/docs/source/examples/PDB/README.txt @@ -0,0 +1,7 @@ + + +Read/Write PDB files +-------------------- + + + diff --git a/docs/source/examples/PDB/read_pdb_errors.py b/docs/source/examples/PDB/read_pdb_errors.py new file mode 100644 index 00000000..06efda36 --- /dev/null +++ b/docs/source/examples/PDB/read_pdb_errors.py @@ -0,0 +1,26 @@ +""" + +PDB read with errors +-------------------- + +If your PDB is arbitrary formatted it might be successfully read but +the result might be unexpected. + +Most likely you will get messed atom/residue ids, wrong coordinates or if you are lucky an Exception which will tell you +what is wrong with your PDB file. + +Please see how to read non-standard, but well-formatted pdb file: :ref:`read-write-non-standard-pdb` + +""" + +import pyxmolpp2 +import os + +pdb_filename = os.path.join(os.environ["TEST_DATA_PATH"], "trjtool/GB1/run00001.pdb") + +# Define a PDB file with standard records fields layout +pdb_file = pyxmolpp2.pdb.PdbFile(pdb_filename) + +############################################################################## +# Failed read: +frame = pdb_file.get_frame() diff --git a/docs/source/examples/PDB/read_write_non_standard_pdb.py b/docs/source/examples/PDB/read_write_non_standard_pdb.py new file mode 100644 index 00000000..b246d0af --- /dev/null +++ b/docs/source/examples/PDB/read_write_non_standard_pdb.py @@ -0,0 +1,45 @@ +""" + +.. _read-write-non-standard-pdb: + +Non-standard PDB io +------------------- + +``pyxmolpp2`` supports reading/writing non-standard PDB files. To read such file you need to specify +a set of new records (usually based on :py:class:`~pyxmolpp2.pdb.StandardPdbRecords` records). + +Full set of available :py:class:`~pyxmolpp2.pdb.RecordName`, :py:class:`~pyxmolpp2.pdb.FiledName` and standard values you can find `here `_ + + +""" + +import pyxmolpp2 +from pyxmolpp2.pdb import AlteredPdbRecords, StandardPdbRecords, FieldName, RecordName, PdbFile +import os + +# Create PDB records description based on standard records +altered_records = AlteredPdbRecords(pyxmolpp2.pdb.StandardPdbRecords.instance()) + +# Expand ATOM.serial record to columns 7-12 +altered_records.alter_record(RecordName("ATOM"), FieldName("serial"), [7, 12]) + +pdb_filename = os.path.join(os.environ["TEST_DATA_PATH"], "trjtool/GB1/run00001.pdb") + +# Define a PDB file with altered records fields layout +pdb_file = PdbFile(pdb_filename, altered_records) + +# Read all frames (i.e. MODELS) from PDB +frames = pdb_file.get_frames() + +# Print some info about frames +print("PDB contains %d MODELS: " % len(frames)) +for frame in frames: + print("\tMODEL #%d contains %d molecules, total %d atoms" % ( + frame.index, + frame.asChains.size, + frame.asAtoms.size + )) + + +# Write first model to PDB file using altered records +frames[0].to_pdb("out.pdb", altered_records) diff --git a/docs/source/examples/PDB/read_write_standard_pdb.py b/docs/source/examples/PDB/read_write_standard_pdb.py new file mode 100644 index 00000000..167190d6 --- /dev/null +++ b/docs/source/examples/PDB/read_write_standard_pdb.py @@ -0,0 +1,31 @@ +""" + +Standard PDB io +--------------- + +""" + +import pyxmolpp2 +import os + +pdb_filename = os.path.join(os.environ["TEST_DATA_PATH"], "pdb/rcsb/1PGB.pdb") + +# Define a PDB file with standard records fields layout +pdb_file = pyxmolpp2.pdb.PdbFile(pdb_filename) + +# Read all frames (i.e. MODELS) from PDB +frames = pdb_file.get_frames() + + +# Print some info about frames +print("PDB contains %d MODELS: " % len(frames)) +for frame in frames: + print("\tMODEL #%d contains %d molecules, total %d atoms" % ( + frame.index, + frame.asChains.size, + frame.asAtoms.size + )) + + +# Write first model to PDB file using altered records +frames[0].to_pdb("out.pdb") diff --git a/docs/source/examples/README.txt b/docs/source/examples/README.txt new file mode 100644 index 00000000..830dc0cb --- /dev/null +++ b/docs/source/examples/README.txt @@ -0,0 +1,6 @@ + + +Examples gallery +================ + +Here you can find typical usages of the library \ No newline at end of file diff --git a/docs/source/examples/basics/FCRA.py b/docs/source/examples/basics/FCRA.py new file mode 100644 index 00000000..d70ef839 --- /dev/null +++ b/docs/source/examples/basics/FCRA.py @@ -0,0 +1,123 @@ +""" + + +.. |Atom| replace:: :py:class:`~pyxmolpp2.polymer.Atom` +.. |Residue| replace:: :py:class:`~pyxmolpp2.polymer.Residue` +.. |Chain| replace:: :py:class:`~pyxmolpp2.polymer.Chain` +.. |Frame| replace:: :py:class:`~pyxmolpp2.polymer.Frame` + +.. |AtomSelection| replace:: :py:class:`~pyxmolpp2.polymer.AtomSelection` +.. |ResidueSelection| replace:: :py:class:`~pyxmolpp2.polymer.ResidueSelection` +.. |ChainSelection| replace:: :py:class:`~pyxmolpp2.polymer.ChainSelection` + + + +Frame/Chain/Residue/Atom hierarchy +---------------------------------- + + +The `pyxmolpp2` library implements |Frame|/|Chain|/|Residue|/|Atom| hierarchy to represent a molecular system. + +Every |Atom| exists as a part of some |Residue|. |Residue| is always a part of |Chain|. |Chain| is always a part of |Frame|. + +For instance this implies that you can not create an |Atom| without pre-existed |Residue|. +Why not allow ``Atom.residue`` to be :py:class:`None` if Atom exists by it's own? +The choice was between flexibility of construction code versus complexity of it's further usage. +The rationale behind that design decision is fact that expression ``atom.residue.chain.frame.index`` +is always correct, and no `not-None` checks are required. Such checks across the library and user code would +increases it's complexity and make it more prone to errors. + + + +.. danger:: + |Atom|/|Residue|/|Chain| is alive until it's |Frame| exists. |Frame| exists until python holds a reference to it. + No frame - no game. + + +""" + + +import pyxmolpp2 +import os + +pdb_filename = os.path.join(os.environ["TEST_DATA_PATH"], "pdb/rcsb/1UBQ.pdb") +pdb_file = pyxmolpp2.pdb.PdbFile(pdb_filename) + +frame = pdb_file.get_frame() + +############################################################################## + +# print chain names of 1UBQ entry +print([ chain.name.str for chain in frame.asChains ]) + +############################################################################## + +# print residue names of 1UBQ entry +print([ res.name.str for res in frame.asResidues]) + +############################################################################## +# print info about first atom: +a = frame.asAtoms[0] +from tabulate import tabulate + +print(tabulate([ + ("name", a.name), + ("id", a.id), + ("[x,y,z]", a.r), + ("rId", a.rId), + ("rName", a.rName), + ("cName", a.cName), +])) + +############################################################################## +# we can find frame by atom +assert a.frame == frame + +############################################################################## +# If you destroy all references to frame it will be eliminated with all it's content + +asel = frame.asAtoms # valid as long reference to frame exists +rsel = frame.asResidues # valid as long reference to frame exists +csel = frame.asChains # valid as long reference to frame exists +a = frame.asAtoms[0] # valid as long reference to frame exists + +############################################################################## +# Let's `accidentally` destroy frame, by dropping only reference: +frame = None + + +############################################################################## +try: + print(asel[0]) +except pyxmolpp2.polymer.DeadAtomSelectionAccess as e: + print("AtomSelection error:") + print(e) + pass + +############################################################################## +try: + print(rsel[0]) +except pyxmolpp2.polymer.DeadResidueSelectionAccess as e: + print("ResidueSelection error:") + print(e) + pass + +############################################################################## +try: + print(csel[0]) +except pyxmolpp2.polymer.DeadChainSelectionAccess as e: + print("ChainSelection error:") + print(e) + pass + +############################################################################## + + + +############################################################################## +try: + print(a.name) +except RuntimeError as e: + print("Atom access error:") + print(e) + pass \ No newline at end of file diff --git a/docs/source/examples/basics/README.txt b/docs/source/examples/basics/README.txt new file mode 100644 index 00000000..7d3c2564 --- /dev/null +++ b/docs/source/examples/basics/README.txt @@ -0,0 +1,3 @@ + +Basics +------ diff --git a/docs/source/examples/basics/RMSD.py b/docs/source/examples/basics/RMSD.py new file mode 100644 index 00000000..0328cc95 --- /dev/null +++ b/docs/source/examples/basics/RMSD.py @@ -0,0 +1,120 @@ +""" + +.. |Atom| replace:: :py:class:`~pyxmolpp2.polymer.Atom` +.. |Residue| replace:: :py:class:`~pyxmolpp2.polymer.Residue` +.. |Chain| replace:: :py:class:`~pyxmolpp2.polymer.Chain` +.. |Frame| replace:: :py:class:`~pyxmolpp2.polymer.Frame` + +.. |AtomSelection| replace:: :py:class:`~pyxmolpp2.polymer.AtomSelection` +.. |ResidueSelection| replace:: :py:class:`~pyxmolpp2.polymer.ResidueSelection` +.. |ChainSelection| replace:: :py:class:`~pyxmolpp2.polymer.ChainSelection` + +.. |AtomPredicate| replace:: :py:class:`~pyxmolpp2.polymer.AtomPredicate` +.. |ResiduePredicate| replace:: :py:class:`~pyxmolpp2.polymer.ResiduePredicate` +.. |ChainPredicate| replace:: :py:class:`~pyxmolpp2.polymer.ChainPredicate` + + + +RMSD/RMSF calculation +--------------------- + +Calculate RMSD/RMSF + +""" + +import pyxmolpp2 +from pyxmolpp2.polymer import aName +from pyxmolpp2.geometry import calc_rmsd, calc_alignment +import os + +############################################################################## +# Let's create a frame to work with + +pdb_filename = os.path.join(os.environ["TEST_DATA_PATH"], "pdb/rcsb/5BMG.pdb") +pdb_file = pyxmolpp2.pdb.PdbFile(pdb_filename) +frame = pdb_file.get_frame() + +############################################################################## +# Number of residues in chains must be same (strip water, ions, etc.) +N = 0 +for i in range(frame.size): + if frame.asChains[i].size != frame.asChains[0].size: + break + N += 1 + +############################################################################## +# Print RMSD matrix for all deposited chains: +for i in range(0, N): + chain_i_ca = frame.asChains[i].asAtoms.filter(aName == "CA") + + for j in range(0, i + 1): + chain_j_ca = frame.asChains[j].asAtoms.filter(aName == "CA") + + alignment = calc_alignment(chain_i_ca.toCoords, chain_j_ca.toCoords) + rmsd = calc_rmsd(chain_i_ca.toCoords, chain_j_ca.toCoords, alignment) + + print("%5.1f" % rmsd, end=" ") + + print() + + +############################################################################## +# Calculate RMSF per residue + +from pyxmolpp2.geometry import UniformScale3d + +first_chain_ca = frame.asChains[0].asAtoms.filter(aName == "CA") + +# initialize average coordinates with (0,0,0) +avg_coords = first_chain_ca.toCoords.transform(UniformScale3d(0)) + +# calculate average coordinates across N frames +for i in range(0, N): + chain_i_ca = frame.asChains[i].asAtoms.filter(aName == "CA") + chain_i_ca.transform(calc_alignment(first_chain_ca.toCoords, chain_i_ca.toCoords)) + + for k, a in enumerate(chain_i_ca): + avg_coords[k] += a.r + +avg_coords.transform(UniformScale3d(1/N)) + +# align to average coordinates +for i in range(0, N): + chain_i_ca = frame.asChains[i].asAtoms.filter(aName == "CA") + chain_i_ca.transform(calc_alignment(avg_coords, chain_i_ca.toCoords)) + +# calculate per residue RMSF + +import numpy as np + +rmsf = np.zeros((first_chain_ca.size,) ) +for i in range(0, N): + chain_i_ca = frame.asChains[i].asAtoms.filter(aName == "CA") + for k, a in enumerate(chain_i_ca): + rmsf[k] += (a.r-avg_coords[k]).len2() + +rmsf = np.sqrt(rmsf/N) + +############################################################################## +# plot RMSF +import matplotlib.pyplot as plt + +plt.figure(dpi=150) +plt.step(range(len(rmsf)), rmsf, where="mid") +plt.ylabel("RMSF, $\AA$") +plt.grid(color="#CCCCCC",lw=0.1) + +def to_label(a): + from Bio.PDB.Polypeptide import three_to_one + if a.rId.serial%5==0: + return "%s\n%d"%(three_to_one(a.rName.str), a.rId.serial) + else: + return "%s"%(three_to_one(a.rName.str)) + +plt.xticks(range(len(rmsf)), + [ to_label(a) for a in first_chain_ca], + rotation=0,fontsize="x-small") + + + + diff --git a/docs/source/examples/basics/angles.py b/docs/source/examples/basics/angles.py new file mode 100644 index 00000000..96b56db8 --- /dev/null +++ b/docs/source/examples/basics/angles.py @@ -0,0 +1,42 @@ +""" + +AngleValue +---------- + +Protect yourself from missing 2pi/180.0 + + +""" + +from pyxmolpp2.geometry import Degrees, Radians, cos, sin, tan, fabs +import numpy as np + +############################################################################## +# To avoid accidental errors user is forced to use :py:class:`~pyxmolpp2.geometry.AngleValue` instead of raw float numbers +# +# :py:class:`~pyxmolpp2.geometry.AngleValue` can be constructed via :py:class:`~pyxmolpp2.geometry.Degrees` +# or :py:class:`~pyxmolpp2.geometry.Radians`: + +angle_value_1 = Degrees(45) +angle_value_2 = Radians(np.pi) + +############################################################################## +# It can be casted back to float as degrees or radians: + +print(angle_value_1.degrees, angle_value_1.radians) +print(angle_value_2.degrees, angle_value_2.radians) + +############################################################################## +# AngleValue supports all basic arithmetic operations: + +print((angle_value_1*2 + angle_value_2/3).degrees) + +############################################################################## +# :py:mod:`pyxmolpp2.geometry` also defines :py:func:`~pyxmolpp2.geometry.cos`, :py:func:`~pyxmolpp2.geometry.sin`, :py:func:`~pyxmolpp2.geometry.tan`, :py:func:`~pyxmolpp2.geometry.fabs` for convenience: + +print( cos(angle_value_1), + sin(angle_value_1), + tan(angle_value_1), + fabs(angle_value_1).degrees) + +print("380 deg casted to range[0..2pi]:", Degrees(380).to_standard_range().degrees ) \ No newline at end of file diff --git a/docs/source/examples/basics/reorder_atoms.py b/docs/source/examples/basics/reorder_atoms.py new file mode 100644 index 00000000..3c9aea4e --- /dev/null +++ b/docs/source/examples/basics/reorder_atoms.py @@ -0,0 +1,68 @@ +""" + +.. |Atom| replace:: :py:class:`~pyxmolpp2.polymer.Atom` +.. |Residue| replace:: :py:class:`~pyxmolpp2.polymer.Residue` +.. |Chain| replace:: :py:class:`~pyxmolpp2.polymer.Chain` +.. |Frame| replace:: :py:class:`~pyxmolpp2.polymer.Frame` + +.. |AtomSelection| replace:: :py:class:`~pyxmolpp2.polymer.AtomSelection` +.. |ResidueSelection| replace:: :py:class:`~pyxmolpp2.polymer.ResidueSelection` +.. |ChainSelection| replace:: :py:class:`~pyxmolpp2.polymer.ChainSelection` + +.. |AtomPredicate| replace:: :py:class:`~pyxmolpp2.polymer.AtomPredicate` +.. |ResiduePredicate| replace:: :py:class:`~pyxmolpp2.polymer.ResiduePredicate` +.. |ChainPredicate| replace:: :py:class:`~pyxmolpp2.polymer.ChainPredicate` + + + +Reorder atoms/residues +---------------------- + +In this example we will see how to reorder atoms/residues/chains + +""" + +import pyxmolpp2 +import os + +############################################################################## +# Let's create a frame to work with + +pdb_filename = os.path.join(os.environ["TEST_DATA_PATH"], "pdb/rcsb/1UBQ.pdb") +pdb_file = pyxmolpp2.pdb.PdbFile(pdb_filename) + +frame = pdb_file.get_frame() + +# Original atom ids: +print([a.id for a in frame.asAtoms]) + +############################################################################## +# First we need to define new order of atoms. +# For sake of simplicity let's number them in reverse order + +atoms = frame.asAtoms + +for i, a in enumerate(atoms): + a.id = atoms.size - i + +# New atom ids: +print([a.id for a in frame.asAtoms]) + +############################################################################## +# As you can see `atom.id` does not affect atom order. +# To change that we need to construct new Frame with desired order of atoms: + +from pyxmolpp2.polymer import Frame + +new_frame = Frame(0) # create empty frame with index=0 + +for chain in frame.asChains: + new_chain = new_frame.emplace(chain.name) # create empty chain with same name + for residue in frame.asResidues: + new_residue = new_chain.emplace(residue.name, residue.id) # create empty residue with same name and id + for a in sorted(list(residue.asAtoms), key=lambda a: a.id): + new_atom = new_residue.emplace(a) # create a copy of atom `a` + +# New frame atoms ids: +print([a.id for a in new_frame.asAtoms]) + diff --git a/docs/source/examples/basics/selections.py b/docs/source/examples/basics/selections.py new file mode 100644 index 00000000..1eed3cbf --- /dev/null +++ b/docs/source/examples/basics/selections.py @@ -0,0 +1,175 @@ +""" + +.. |Atom| replace:: :py:class:`~pyxmolpp2.polymer.Atom` +.. |Residue| replace:: :py:class:`~pyxmolpp2.polymer.Residue` +.. |Chain| replace:: :py:class:`~pyxmolpp2.polymer.Chain` +.. |Frame| replace:: :py:class:`~pyxmolpp2.polymer.Frame` + +.. |AtomSelection| replace:: :py:class:`~pyxmolpp2.polymer.AtomSelection` +.. |ResidueSelection| replace:: :py:class:`~pyxmolpp2.polymer.ResidueSelection` +.. |ChainSelection| replace:: :py:class:`~pyxmolpp2.polymer.ChainSelection` + +.. |AtomPredicate| replace:: :py:class:`~pyxmolpp2.polymer.AtomPredicate` +.. |ResiduePredicate| replace:: :py:class:`~pyxmolpp2.polymer.ResiduePredicate` +.. |ChainPredicate| replace:: :py:class:`~pyxmolpp2.polymer.ChainPredicate` + + + +Selections +---------- + +*Selection* is ordered set of elements in ``pyxmolpp2``. Order is defined as follows + +1. if two elements belongs to same parent object, the order match their construction order +2. otherwise, they ordered as their parents +3. |Frame| references are ordered by :py:attr:`~pyxmolpp2.polymer.Frame.index` + +""" + +import pyxmolpp2 +import os + +############################################################################## +# Let's create a frame to work with + +pdb_filename = os.path.join(os.environ["TEST_DATA_PATH"], "pdb/rcsb/1UBQ.pdb") +pdb_file = pyxmolpp2.pdb.PdbFile(pdb_filename) + +frame = pdb_file.get_frame() + +############################################################################## +# Library has three types of `selections`: |AtomSelection|, |ResidueSelection| and |ChainSelection| +# +# Construction +# ^^^^^^^^^^^^ +# +# Any selections might be created from |Frame| instance: + +print(frame.asChains) +print(frame.asResidues) +print(frame.asAtoms) + +############################################################################## +# |AtomSelection| and |ResidueSelection| can be created from |Chain|: + +chain = frame.asChains[0] +print(chain.asResidues) +print(chain.asAtoms) + +############################################################################## +# |AtomSelection| can be created from a |Residue|: + +residue = frame.asResidues[0] +print(residue.asAtoms) + +############################################################################## +# Conversions +# ^^^^^^^^^^^ +# +# Selections might be converted up and down through hierarchy: + +print(chain.asAtoms.asResidues) # selects non-empty residues +print(frame.asResidues.asChains) # selects chains with at least 1 residue +print(frame.asChains.asResidues.asAtoms.asResidues.asChains) # select chains with at least 1 non-empty residue + +############################################################################## +# +# Filter +# ^^^^^^ +# ``filter`` method return new selection with elements that match predicate: +# + +from pyxmolpp2.polymer import AtomName, ResidueName + +chain.asAtoms.filter(lambda a: a.r.x < 0) # select atoms with negative x coord +chain.asAtoms.filter(lambda a: a.name == AtomName("CA")) # select CA atoms +chain.asResidues.filter(lambda r: r.name == ResidueName("LYS")) # select LYS residues + +############################################################################## +# +# ``pyxmolpp2` defines predicate-generators which return predicate when compared to something: +# +from pyxmolpp2.polymer import aName, rName, aId, rId, cName, cIndex + +frame.asAtoms.filter(aName == "CA") # select CA atoms +frame.asResidues.filter(rName == "LYS") # select LYS residues +frame.asChains.filter(cName == "A") # select chain(s) A + +############################################################################## +# |ChainPredicate| and |ResiduePredicate| can be naturally applied to |AtomSelection|, +# while |AtomPredicate| can be applied only to |AtomSelection|. + +frame.asAtoms.filter(aName == "CA") # select CA atoms +frame.asAtoms.filter(rName == "LYS") # select all atoms of LYS residues + +############################################################################## +# Application of |AtomPredicate| to |ResidueSelection| or |ChainSelection| leads to :py:class:`TypeError` exception. +# Same stands for |ResiduePredicate| and |ChainSelection|: + +try: + print(frame.asResidues.filter(aName == "CA")) +except TypeError as e: + print(e) +############################################################################## + +try: + print(frame.asChains.filter(aName == "CA")) +except TypeError as e: + print(e) +############################################################################## + +try: + print(frame.asChains.filter(rName == "LYS")) +except TypeError as e: + print(e) +############################################################################## + + +############################################################################## +# Predicates can be combined using `&``, ``|``, ``~`` operators and reused: + +from pyxmolpp2.polymer import AtomPredicate + +criteria = (aName == "CA") & rId.is_in({1, 2, 3, 4}) & AtomPredicate(lambda a: a.r.x < 0) # type:AtomPredicate + +print(frame.asAtoms.filter(criteria | cName.is_in({"X", "Y", "Z"}))) + +############################################################################## +# Set operations +# ^^^^^^^^^^^^^^ +# +# Selections support number set operations: +# - `union` (operators ``+``, ``+=``) +# - `set intersection` (operators ``*``, ``*=``) +# - `difference` (operators ``-``, ``-=``) +# +# +A = frame.asAtoms.filter(lambda a: a.r.x > 5) +B = frame.asAtoms.filter(lambda a: a.r.x <= 5) + +print(A) +print(B) +print(A+B) +print(A-B) +print(A*B) + +############################################################################## +# Invalidation of selection +# ^^^^^^^^^^^^^^^^^^^^^^^^^ +# +# In execution of the program selection may be marked as `invalid`, i.e. further access +# to it's elements raises an exception. +# +# Selection gets invalidated on: +# +# - destruction of any of it's elements parent (Primary this happens on deletion of a whole |Frame|. +# +# .. note:: +# The exception will be raised so you will know that you are doing something wrong. +# +# - on :py:attr:`~pyxmolpp2.polymer.Frame.index` change if selection had elements from two frames or more. +# +# .. danger:: +# Currently there is no runtime checks against this type of errors for sake of performance. +# Please make sure you don't do that + diff --git a/docs/source/examples/basics/torsion_angle.py b/docs/source/examples/basics/torsion_angle.py new file mode 100644 index 00000000..134026f6 --- /dev/null +++ b/docs/source/examples/basics/torsion_angle.py @@ -0,0 +1,112 @@ +""" + +.. |Atom| replace:: :py:class:`~pyxmolpp2.polymer.Atom` +.. |Residue| replace:: :py:class:`~pyxmolpp2.polymer.Residue` +.. |Chain| replace:: :py:class:`~pyxmolpp2.polymer.Chain` +.. |Frame| replace:: :py:class:`~pyxmolpp2.polymer.Frame` + +.. |AtomSelection| replace:: :py:class:`~pyxmolpp2.polymer.AtomSelection` +.. |ResidueSelection| replace:: :py:class:`~pyxmolpp2.polymer.ResidueSelection` +.. |ChainSelection| replace:: :py:class:`~pyxmolpp2.polymer.ChainSelection` + +.. |AtomPredicate| replace:: :py:class:`~pyxmolpp2.polymer.AtomPredicate` +.. |ResiduePredicate| replace:: :py:class:`~pyxmolpp2.polymer.ResiduePredicate` +.. |ChainPredicate| replace:: :py:class:`~pyxmolpp2.polymer.ChainPredicate` + + + +Torsion Angles +-------------- + +""" + +import pyxmolpp2 +import os + +############################################################################## +# Let's create a frame to work with + +pdb_filename = os.path.join(os.environ["TEST_DATA_PATH"], "pdb/rcsb/1UBQ.pdb") +pdb_file = pyxmolpp2.pdb.PdbFile(pdb_filename) + +frame = pdb_file.get_frame() + +############################################################################## +# Standard torsion angles +# ^^^^^^^^^^^^^^^^^^^^^^^ +# +# For standard protein residues angles can be constructed using `TorsionAngleFactory`: + +from pyxmolpp2.polymer import TorsionAngleFactory +from pyxmolpp2.geometry import Degrees + +residue48 = frame.asChains[0][48] +print(residue48) + +psi_48 = TorsionAngleFactory.psi(residue48) +print(psi_48) +print(psi_48.value().degrees) + +############################################################################## +# Note: Factory may return ``None`` if such angle does not exist: +print(TorsionAngleFactory.omega(frame.asResidues[0])) + +############################################################################## +# Torsion angle allows to set a new one: + +# All residues 49-76 are affected by this rotation +psi_48.set(Degrees(150), Degrees(0)) +print(psi_48.value().degrees) + +############################################################################## +# Construction +# ^^^^^^^^^^^^ +# Torsion angle constructor allow two forms: +# 1. Read-only torsion angle +# 2. Read-write torsion angle + +from pyxmolpp2.polymer import TorsionAngle, AtomName, Atom + +r1 = frame.asResidues[1] +r2 = frame.asResidues[2] + +# Let's create a read-only phi of residue 2 +phi_2_ro = TorsionAngle(r1[AtomName("C")], + r2[AtomName("N")], + r2[AtomName("CA")], + r2[AtomName("C")], + ) + +# Check against factory angle: +assert phi_2_ro.value().degrees == TorsionAngleFactory.phi(r2).value().degrees + +############################################################################## +# Attempt to set TorsionAngle will lead to ``RuntimeError``: +try: + phi_2_ro.set(Degrees(-130), Degrees(0)) +except RuntimeError as e: + print(e) + + +############################################################################## +# We need a helper function which returns a selection of affected atoms +# by our torsion angle + +def affected_phi_atoms(a: Atom, b: Atom, c: Atom, d: Atom): + from pyxmolpp2.polymer import rId + return a.chain.asResidues.filter(rId > a.rId).asAtoms + + +phi_2_rw = TorsionAngle(r1[AtomName("C")], + r2[AtomName("N")], + r2[AtomName("CA")], + r2[AtomName("C")], + affected_phi_atoms + ) + +phi_2_rw.set(Degrees(-130), Degrees(0)) + +print(phi_2_ro.value().degrees) +print(phi_2_rw.value().degrees) + + diff --git a/docs/source/images/python-logo.png b/docs/source/images/python-logo.png new file mode 100644 index 00000000..1e4c08d0 Binary files /dev/null and b/docs/source/images/python-logo.png differ diff --git a/docs/source/images/python-logo.svg b/docs/source/images/python-logo.svg new file mode 100644 index 00000000..23bd5a23 --- /dev/null +++ b/docs/source/images/python-logo.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index 800ae0e4..0e559221 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -6,19 +6,14 @@ Welcome to pyxmolpp2's documentation! ===================================== -.. toctree:: - :caption: Tutorial - :maxdepth: 2 - :glob: +.. include:: install.rst - tutorial/* .. toctree:: - :caption: Examples :maxdepth: 2 - :glob: + :caption: Examples - examples/* + auto_examples/index .. toctree:: :caption: API Reference diff --git a/docs/source/install.rst b/docs/source/install.rst new file mode 100644 index 00000000..60a467d6 --- /dev/null +++ b/docs/source/install.rst @@ -0,0 +1,57 @@ + +Installation +------------ + +from PyPi package +^^^^^^^^^^^^^^^^^ + +.. warning:: + + If your default C++ compiler does not have full C++11 support you will see messy error + message during installation and you must set environment variables to point to modern + version of C++ compiler (g++>=5.0 or clang>=3.6), for example: + + .. code-block:: bash + + export CC=gcc-7.3.0 ; export CXX=g++-7.3.0 ; + + + +PyPi package contains all C++ dependencies, total size is about ~4 mb. +Install command: + +.. code-block:: bash + + python -m pip install pyxmolpp2 + + + +from git repository +^^^^^^^^^^^^^^^^^^^ + +.. note:: + + Total size of repository with sub-repositories exceeds 100mb + +To install from master branch run command + +.. code-block:: bash + + python -m pip install git+https://github.com/sizmailov/pyxmolpp2.git + + +Generation stubs for PyCharm/mypy +--------------------------------- + +``pyxmolpp2`` is a binary python module, so it can't be readily understood by static analysis tools. +The ``pyxmolpp2-stubs`` package generates stubs for installed ``pyxmolpp2`` module. + + +.. code-block:: bash + + # install from PyPI + python -m pip install pyxmolpp2-stubs + + # install from github + python -m pip install git+https://github.com/sizmailov/pyxmolpp2-stubs.git + diff --git a/docs/source/tutorial/first_steps.rst b/docs/source/tutorial/first_steps.rst deleted file mode 100644 index 5f4086d7..00000000 --- a/docs/source/tutorial/first_steps.rst +++ /dev/null @@ -1,226 +0,0 @@ - -.. |Atom| replace:: :py:class:`~pyxmolpp2.polymer.Atom` -.. |Residue| replace:: :py:class:`~pyxmolpp2.polymer.Residue` -.. |Chain| replace:: :py:class:`~pyxmolpp2.polymer.Chain` -.. |Frame| replace:: :py:class:`~pyxmolpp2.polymer.Frame` - -.. |AtomSelection| replace:: :py:class:`~pyxmolpp2.polymer.AtomSelection` -.. |ResidueSelection| replace:: :py:class:`~pyxmolpp2.polymer.ResidueSelection` -.. |ChainSelection| replace:: :py:class:`~pyxmolpp2.polymer.ChainSelection` - - - -First steps -=========== - - -Installation ------------- - -from PyPi package -^^^^^^^^^^^^^^^^^ - -.. warning:: - - If your default C++ compiler does not have full C++11 support you will see messy error - message during installation and you must set environment variables to point to modern - version of C++ compiler (g++>=5.0 or clang>=3.6), for example: - - .. code-block:: bash - - export CC=gcc-7.3.0 ; export CXX=g++-7.3.0 ; - - - -PyPi package contains all C++ dependencies, total size is about ~4 mb. -Install command: - -.. code-block:: bash - - python -m pip install pyxmolpp2 - - - -from git repository -^^^^^^^^^^^^^^^^^^^ - -**Caution**: Total size of repository with sub-repositories exceeds 100mb - -To install from master branch run command - -.. code-block:: bash - - python -m pip install git+https://github.com/sizmailov/pyxmolpp2.git - - -Generation stubs for PyCharm/mypy ---------------------------------- - -``pyxmolpp2`` is a binary python module, so it can't be readily understood by static analysis tools. -The ``pyxmolpp2-stubs`` package generates stubs for installed ``pyxmolpp2`` module. - - -.. code-block:: bash - - # install from PyPI - python -m pip install pyxmolpp2-stubs - - # install from github - python -m pip install git+https://github.com/sizmailov/pyxmolpp2-stubs.git - - - -Reading pdb file ----------------- - -If your ``.pdb`` file conforms `Atomic Coordinate Entry Format Version 3.3 `_ -you can read it a via :py:class:`pyxmolpp2.pdb.PdbFile`: - -.. code-block:: python - - from pyxmolpp2.pdb import PdbFile - frame = PdbFile("sample.pdb").get_frame() - print(frame.size) - - -Reading non-standard pdb file -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -If you want to read non-standard pdb file you should provide altered pdb records that match your file. -For example one may want to adjust ``serial`` field of ``ATOM`` record: - -.. TODO: check that code actually works -.. code-block:: python - - from pyxmolpp2.pdb import * - altered_records = AlteredPdbRecords(StandardPdbRecords.instance()) - altered_records.alter_record(RecordName("ATOM"),FieldName("serial"),[7,12]) - frame = PdbFile("sample.pdb", altered_records).get_frame() - print(frame.size) - -See :py:class:`~pyxmolpp2.pdb.AlteredPdbRecords` for more detail. - -.. caution:: - Reading of pdb files with variable position of fields is not supported: - - :: - - ATOM 32 N AARG A -3 11.281 86.699 94.383 0.50 35.88 N - ATOM 33 CA AARG A -3 12.353 85.696 94.456 0.50 36.67 C - - -Selections ----------- - -Library has three types of `selections`: |AtomSelection|, |ResidueSelection|, |ChainSelection| - ordered sets of elements. - -Construction -^^^^^^^^^^^^ - -Any selections might be created from |Frame| instance, |AtomSelection| and |ResidueSelection| can be created -from |Chain| instance, |AtomSelection| could be created from a |Residue|:: - - print(frame.asAtoms.size) # number of atoms in frame - print(frame.asResidues.size) # number of residues in frame - print(frame.asChains.size)# number of chains in frame - - chain = frame.asChains[0] - print(chain.asAtoms.size) # number of atoms in first chain - print(chain.asResidues.size) # number of residues in first chain - - residue = frame.asResidues[-1] - print(residue.asAtoms.size) # number of atoms in frame last residue - - -Conversions -^^^^^^^^^^^ - -Selections might be converted up and down thought hierarchy:: - - chain.asAtoms.asResidues # selects non-empty residues - frame.asResidues.asChains # selects chains with at least 1 residue - frame.asChains.asResidues.asAtoms.asResidues.asChains # selects chains with at least 1 non-empty residue - -Filter -^^^^^^ -A selection could be filtered inplace via `filter` method using lambda:: - - chain.asAtoms.filter(lambda a: a.r.x < 0) # select atoms with negative x coord - chain.asAtoms.filter(lambda a: a.name == AtomName("CA")) # select CA atoms - chain.asResidues.filter(lambda r: r.name == ResidueName("LYS")) # select LYS residues - - -or using pre-defined predicate-generators:: - - from pyxmolpp2.polymer import aName, rName - - chain.asAtoms.filter(aName == "CA") # select CA atoms - chain.asResidues.filter(rName == "LYS") # select LYS residues - -predicates can be stored and combined using ``&``, ``|``, ``~`` operators :: - - pred = (aName == "CA") & (rName == "LYS") # create complex atom predicate - - chain.asAtoms.filter(pred) # select atoms CA of LYS - - -Set operations -^^^^^^^^^^^^^^ - -Selections support number set operations: - - `union` (operators ``+``, ``+=``) - - `set intersection` (operators ``*``, ``*=``) - - `difference` (operators ``-``, ``-=``) - - -.. code-block:: python - - A = frame.asAtoms.filter(lambda a: a.x > 0) - B = frame.asAtoms.filter(lambda a: a.x <= 0) - - C = A+B - C = A-B - C += B - - D = A*B - - -Invalidation of selection -^^^^^^^^^^^^^^^^^^^^^^^^^ - -In execution of the program selection may be marked as `invalid`, i.e. further access -to it's elements raises an exception. - -Selection gets invalidated on: - - destruction of any of it's elements parent (Primary this happens on deletion of a whole |Frame|) - - on :py:attr:`~pyxmolpp2.polymer.Frame.index` change if selection had elements from two frames or more - -Atom/Residue/Chain references -============================= - -It's allowed to store references to Atom/Residue/Chain/Frame in python code. They are -guaranteed to be not-None, while they might be invalidated if corresponding structure was destroyed. - -Access to invalid reference results to exception. - - -Strict hierarchy rationale -========================== - -The `pyxmolpp2` library implements |Frame|/|Chain|/|Residue|/|Atom| hierarchy to represent a molecular system. - -Every |Atom| exists as a part of some |Residue|. |Residue| is always a part of |Chain|. |Chain| is always a part of |Frame|. - -For instance this implies that you can not create an |Atom| without pre-existed |Residue|. -Why not allow ``Atom.residue`` to be :py:class:`None` if Atom exists by it's own? -The choice was between flexibility of construction code versus complexity of it's further usage. -The rationale behind that design decision is fact that expression ``atom.residue.chain.frame.index`` -is always correct, and no `not-None` checks are required. Such checks across the library and user code would -increases it's complexity and make it more prone to errors. - - -Keep your frame alive -===================== - -|Atom| is alive until it's |Frame| exists. |Frame| exists until python holds a reference to it. -No frame - no game.