Skip to content

Commit 36435a5

Browse files
authored
Merge pull request #886 from padix-key/nucleotide-dihedrals
Add measurements of nucleotide dihedral angles
2 parents 2e5ef85 + 0189bfe commit 36435a5

6 files changed

Lines changed: 842 additions & 33 deletions

File tree

doc/apidoc.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,9 @@
385385
"base_pairs_glycosidic_bond",
386386
"dot_bracket",
387387
"dot_bracket_from_structure",
388-
"base_pairs_from_dot_bracket"
388+
"base_pairs_from_dot_bracket",
389+
"nucleotide_dihedral_backbone",
390+
"nucleotide_dihedral_side_chain"
389391
],
390392
"Aromatic rings": [
391393
"find_aromatic_rings",

src/biotite/structure/geometry.py

Lines changed: 159 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,20 @@
2020
"index_dihedral",
2121
"dihedral_backbone",
2222
"dihedral_side_chain",
23+
"nucleotide_dihedral_backbone",
24+
"nucleotide_dihedral_side_chain",
2325
"centroid",
2426
]
2527

2628
import functools
2729
import numpy as np
2830
from biotite.structure.atoms import AtomArray, AtomArrayStack, coord
2931
from biotite.structure.box import coord_to_fraction, fraction_to_coord, is_orthogonal
30-
from biotite.structure.filter import filter_amino_acids, filter_canonical_amino_acids
32+
from biotite.structure.filter import (
33+
filter_amino_acids,
34+
filter_canonical_amino_acids,
35+
filter_nucleotides,
36+
)
3137
from biotite.structure.residues import get_residue_starts
3238
from biotite.structure.util import (
3339
coord_for_atom_name_per_residue,
@@ -600,24 +606,9 @@ def dihedral_backbone(atom_array):
600606
coord_for_omg[..., 0:-1, :, 3] = coord_ca[..., 1:, :]
601607
# fmt: on
602608

603-
phi = dihedral(
604-
coord_for_phi[..., 0],
605-
coord_for_phi[..., 1],
606-
coord_for_phi[..., 2],
607-
coord_for_phi[..., 3],
608-
)
609-
psi = dihedral(
610-
coord_for_psi[..., 0],
611-
coord_for_psi[..., 1],
612-
coord_for_psi[..., 2],
613-
coord_for_psi[..., 3],
614-
)
615-
omg = dihedral(
616-
coord_for_omg[..., 0],
617-
coord_for_omg[..., 1],
618-
coord_for_omg[..., 2],
619-
coord_for_omg[..., 3],
620-
)
609+
phi = dihedral(*(coord_for_phi[..., i] for i in range(4)))
610+
psi = dihedral(*(coord_for_psi[..., i] for i in range(4)))
611+
omg = dihedral(*(coord_for_omg[..., i] for i in range(4)))
621612

622613
return phi, psi, omg
623614

@@ -691,18 +682,12 @@ def dihedral_side_chain(atoms):
691682
res_mask = res_names == res_name
692683
for chi_i, chi_atom_names in enumerate(chi_atom_names_for_all_angles):
693684
dihedrals = dihedral(
694-
chi_atom_coord[
695-
chi_atoms_to_coord_index[chi_atom_names[0]], ..., res_mask, :
696-
],
697-
chi_atom_coord[
698-
chi_atoms_to_coord_index[chi_atom_names[1]], ..., res_mask, :
699-
],
700-
chi_atom_coord[
701-
chi_atoms_to_coord_index[chi_atom_names[2]], ..., res_mask, :
702-
],
703-
chi_atom_coord[
704-
chi_atoms_to_coord_index[chi_atom_names[3]], ..., res_mask, :
705-
],
685+
*(
686+
chi_atom_coord[
687+
chi_atoms_to_coord_index[atom_name], ..., res_mask, :
688+
]
689+
for atom_name in chi_atom_names
690+
)
706691
)
707692
if is_multi_model:
708693
# Swap dimensions due to NumPy's behavior when using advanced indexing
@@ -712,6 +697,149 @@ def dihedral_side_chain(atoms):
712697
return chi_angles
713698

714699

700+
def nucleotide_dihedral_backbone(atom_array):
701+
r"""
702+
Measure the six characteristic backbone dihedral angles of a nucleotide chain.
703+
704+
Parameters
705+
----------
706+
atom_array : AtomArray or AtomArrayStack
707+
The nucleic acid structure to measure the dihedral angles for.
708+
For missing backbone atoms the corresponding angles are `NaN`.
709+
710+
Returns
711+
-------
712+
alpha, beta, gamma, delta, epsilon, zeta : ndarray, shape=(m,n) or shape=(n,), dtype=float
713+
An array containing the six backbone dihedral angles for every nucleotide
714+
residue.
715+
:math:`\alpha` is not defined at the 5'-terminus, :math:`\epsilon` and
716+
:math:`\zeta` are not defined at the 3'-terminus.
717+
In these places the arrays have *NaN* values.
718+
If an :class:`AtomArrayStack` is given, the output angles are 2-dimensional,
719+
the first dimension corresponds to the model number.
720+
721+
Notes
722+
-----
723+
The nucleotide backbone dihedral angles are defined as follows
724+
(indices refer to the residue position along the chain):
725+
726+
- :math:`\alpha`: O3'(i-1) - P(i) - O5'(i) - C5'(i)
727+
- :math:`\beta`: P(i) - O5'(i) - C5'(i) - C4'(i)
728+
- :math:`\gamma`: O5'(i) - C5'(i) - C4'(i) - C3'(i)
729+
- :math:`\delta`: C5'(i) - C4'(i) - C3'(i) - O3'(i)
730+
- :math:`\epsilon`: C4'(i) - C3'(i) - O3'(i) - P(i+1)
731+
- :math:`\zeta`: C3'(i) - O3'(i) - P(i+1) - O5'(i+1)
732+
"""
733+
coord_p, coord_o5p, coord_c5p, coord_c4p, coord_c3p, coord_o3p = (
734+
coord_for_atom_name_per_residue(
735+
atom_array,
736+
("P", "O5'", "C5'", "C4'", "C3'", "O3'"),
737+
filter_nucleotides(atom_array),
738+
)
739+
)
740+
n_residues = coord_p.shape[-2]
741+
742+
# Coordinates for dihedral angle calculation
743+
# Dim 0: Model index (only for atom array stacks)
744+
# Dim 1: Angle index
745+
# Dim 2: X, Y, Z coordinates
746+
# Dim 3: Atoms involved in dihedral angle
747+
if isinstance(atom_array, AtomArray):
748+
angle_coord_shape: tuple[int, ...] = (n_residues, 3, 4)
749+
elif isinstance(atom_array, AtomArrayStack):
750+
angle_coord_shape = (atom_array.stack_depth(), n_residues, 3, 4)
751+
(
752+
coord_for_alpha,
753+
coord_for_beta,
754+
coord_for_gamma,
755+
coord_for_delta,
756+
coord_for_epsilon,
757+
coord_for_zeta,
758+
) = [np.full(angle_coord_shape, np.nan, dtype=np.float32) for _ in range(6)]
759+
760+
# fmt: off
761+
coord_for_alpha[..., 1:, :, 0] = coord_o3p[..., 0:-1, :]
762+
coord_for_alpha[..., 1:, :, 1] = coord_p[..., 1:, :]
763+
coord_for_alpha[..., 1:, :, 2] = coord_o5p[..., 1:, :]
764+
coord_for_alpha[..., 1:, :, 3] = coord_c5p[..., 1:, :]
765+
766+
coord_for_beta[..., :, :, 0] = coord_p
767+
coord_for_beta[..., :, :, 1] = coord_o5p
768+
coord_for_beta[..., :, :, 2] = coord_c5p
769+
coord_for_beta[..., :, :, 3] = coord_c4p
770+
771+
coord_for_gamma[..., :, :, 0] = coord_o5p
772+
coord_for_gamma[..., :, :, 1] = coord_c5p
773+
coord_for_gamma[..., :, :, 2] = coord_c4p
774+
coord_for_gamma[..., :, :, 3] = coord_c3p
775+
776+
coord_for_delta[..., :, :, 0] = coord_c5p
777+
coord_for_delta[..., :, :, 1] = coord_c4p
778+
coord_for_delta[..., :, :, 2] = coord_c3p
779+
coord_for_delta[..., :, :, 3] = coord_o3p
780+
781+
coord_for_epsilon[..., 0:-1, :, 0] = coord_c4p[..., 0:-1, :]
782+
coord_for_epsilon[..., 0:-1, :, 1] = coord_c3p[..., 0:-1, :]
783+
coord_for_epsilon[..., 0:-1, :, 2] = coord_o3p[..., 0:-1, :]
784+
coord_for_epsilon[..., 0:-1, :, 3] = coord_p[..., 1:, :]
785+
786+
coord_for_zeta[..., 0:-1, :, 0] = coord_c3p[..., 0:-1, :]
787+
coord_for_zeta[..., 0:-1, :, 1] = coord_o3p[..., 0:-1, :]
788+
coord_for_zeta[..., 0:-1, :, 2] = coord_p[..., 1:, :]
789+
coord_for_zeta[..., 0:-1, :, 3] = coord_o5p[..., 1:, :]
790+
# fmt: on
791+
792+
alpha = dihedral(*(coord_for_alpha[..., i] for i in range(4)))
793+
beta = dihedral(*(coord_for_beta[..., i] for i in range(4)))
794+
gamma = dihedral(*(coord_for_gamma[..., i] for i in range(4)))
795+
delta = dihedral(*(coord_for_delta[..., i] for i in range(4)))
796+
epsilon = dihedral(*(coord_for_epsilon[..., i] for i in range(4)))
797+
zeta = dihedral(*(coord_for_zeta[..., i] for i in range(4)))
798+
799+
return alpha, beta, gamma, delta, epsilon, zeta
800+
801+
802+
def nucleotide_dihedral_side_chain(atoms):
803+
r"""
804+
Measure the glycosidic :math:`\chi` dihedral angle of nucleotide residues.
805+
806+
Parameters
807+
----------
808+
atoms : AtomArray or AtomArrayStack
809+
The nucleic acid structure to measure the glycosidic dihedral angles for.
810+
811+
Returns
812+
-------
813+
chi : ndarray, shape=(m, n) or shape=(n,), dtype=float
814+
An array containing the :math:`\chi` angle for every residue.
815+
Residues that are not nucleotides or lack the required atoms are filled with
816+
:math:`NaN` values.
817+
818+
Notes
819+
-----
820+
The :math:`\chi` angle is defined between the sugar and the base:
821+
822+
- Purines (e.g. ``A``, ``G``): ``O4' - C1' - N9 - C4``
823+
- Pyrimidines (e.g. ``C``, ``U``, ``T``): ``O4' - C1' - N1 - C2``
824+
825+
The base type is inferred from the presence of the ``N9`` atom, so modified
826+
nucleotides are handled as long as they use the canonical glycosidic linkage.
827+
"""
828+
coord_o4p, coord_c1p, coord_n9, coord_c4, coord_n1, coord_c2 = (
829+
coord_for_atom_name_per_residue(
830+
atoms,
831+
("O4'", "C1'", "N9", "C4", "N1", "C2"),
832+
filter_nucleotides(atoms),
833+
)
834+
)
835+
836+
purine_chi = dihedral(coord_o4p, coord_c1p, coord_n9, coord_c4)
837+
pyrimidine_chi = dihedral(coord_o4p, coord_c1p, coord_n1, coord_c2)
838+
# Purines are distinguished from pyrimidines by the presence of the N9 atom
839+
is_pyrimidine = np.isnan(coord_n9[..., 0])
840+
return np.where(is_pyrimidine, pyrimidine_chi, purine_chi)
841+
842+
715843
def centroid(atoms):
716844
"""
717845
Measure the centroid of a structure.

tests/structure/data/misc/dihedrals.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
# MDTraj only outputs the dihedral angles only for residues,
2929
# where they are applicable
3030
# -> Map the angles to the correct residues using the returned indices
31-
# amd keep the inapplicable residues as NaN
31+
# and keep the inapplicable residues as NaN
3232
mapped_dihedrals = np.full((struc.get_residue_count(atoms)), np.nan)
3333
# Use the second atom of each angle to infer the residue,
3434
# to handle the edge case of 'phi'

0 commit comments

Comments
 (0)