From 7ac75e6f7b7d5b23aa0560563224c037739ab770 Mon Sep 17 00:00:00 2001 From: Patrick Kunzmann Date: Thu, 16 Apr 2026 12:13:53 +0200 Subject: [PATCH 1/2] Decrease code duplication --- src/biotite/structure/geometry.py | 39 +++++++------------------------ 1 file changed, 9 insertions(+), 30 deletions(-) diff --git a/src/biotite/structure/geometry.py b/src/biotite/structure/geometry.py index 318d241bf..889fc6224 100644 --- a/src/biotite/structure/geometry.py +++ b/src/biotite/structure/geometry.py @@ -600,24 +600,9 @@ def dihedral_backbone(atom_array): coord_for_omg[..., 0:-1, :, 3] = coord_ca[..., 1:, :] # fmt: on - phi = dihedral( - coord_for_phi[..., 0], - coord_for_phi[..., 1], - coord_for_phi[..., 2], - coord_for_phi[..., 3], - ) - psi = dihedral( - coord_for_psi[..., 0], - coord_for_psi[..., 1], - coord_for_psi[..., 2], - coord_for_psi[..., 3], - ) - omg = dihedral( - coord_for_omg[..., 0], - coord_for_omg[..., 1], - coord_for_omg[..., 2], - coord_for_omg[..., 3], - ) + phi = dihedral(*(coord_for_phi[..., i] for i in range(4))) + psi = dihedral(*(coord_for_psi[..., i] for i in range(4))) + omg = dihedral(*(coord_for_omg[..., i] for i in range(4))) return phi, psi, omg @@ -691,18 +676,12 @@ def dihedral_side_chain(atoms): res_mask = res_names == res_name for chi_i, chi_atom_names in enumerate(chi_atom_names_for_all_angles): dihedrals = dihedral( - chi_atom_coord[ - chi_atoms_to_coord_index[chi_atom_names[0]], ..., res_mask, : - ], - chi_atom_coord[ - chi_atoms_to_coord_index[chi_atom_names[1]], ..., res_mask, : - ], - chi_atom_coord[ - chi_atoms_to_coord_index[chi_atom_names[2]], ..., res_mask, : - ], - chi_atom_coord[ - chi_atoms_to_coord_index[chi_atom_names[3]], ..., res_mask, : - ], + *( + chi_atom_coord[ + chi_atoms_to_coord_index[atom_name], ..., res_mask, : + ] + for atom_name in chi_atom_names + ) ) if is_multi_model: # Swap dimensions due to NumPy's behavior when using advanced indexing From 0189bfea9f1336fbf037a7e0addf2a83a4d6a46e Mon Sep 17 00:00:00 2001 From: Patrick Kunzmann Date: Thu, 16 Apr 2026 16:00:02 +0200 Subject: [PATCH 2/2] Add `nucleotide_dihedral_backbone` and `nucleotide_dihedral_side_chain` --- doc/apidoc.json | 4 +- src/biotite/structure/geometry.py | 151 ++++- tests/structure/data/misc/dihedrals.py | 2 +- .../data/misc/nucleotide_dihedrals.json | 597 ++++++++++++++++++ .../data/misc/nucleotide_dihedrals.py | 24 + tests/structure/test_geometry.py | 58 ++ 6 files changed, 833 insertions(+), 3 deletions(-) create mode 100644 tests/structure/data/misc/nucleotide_dihedrals.json create mode 100644 tests/structure/data/misc/nucleotide_dihedrals.py diff --git a/doc/apidoc.json b/doc/apidoc.json index 407893c98..7551ec6ab 100644 --- a/doc/apidoc.json +++ b/doc/apidoc.json @@ -385,7 +385,9 @@ "base_pairs_glycosidic_bond", "dot_bracket", "dot_bracket_from_structure", - "base_pairs_from_dot_bracket" + "base_pairs_from_dot_bracket", + "nucleotide_dihedral_backbone", + "nucleotide_dihedral_side_chain" ], "Aromatic rings": [ "find_aromatic_rings", diff --git a/src/biotite/structure/geometry.py b/src/biotite/structure/geometry.py index 889fc6224..2a5aabb8b 100644 --- a/src/biotite/structure/geometry.py +++ b/src/biotite/structure/geometry.py @@ -20,6 +20,8 @@ "index_dihedral", "dihedral_backbone", "dihedral_side_chain", + "nucleotide_dihedral_backbone", + "nucleotide_dihedral_side_chain", "centroid", ] @@ -27,7 +29,11 @@ import numpy as np from biotite.structure.atoms import AtomArray, AtomArrayStack, coord from biotite.structure.box import coord_to_fraction, fraction_to_coord, is_orthogonal -from biotite.structure.filter import filter_amino_acids, filter_canonical_amino_acids +from biotite.structure.filter import ( + filter_amino_acids, + filter_canonical_amino_acids, + filter_nucleotides, +) from biotite.structure.residues import get_residue_starts from biotite.structure.util import ( coord_for_atom_name_per_residue, @@ -691,6 +697,149 @@ def dihedral_side_chain(atoms): return chi_angles +def nucleotide_dihedral_backbone(atom_array): + r""" + Measure the six characteristic backbone dihedral angles of a nucleotide chain. + + Parameters + ---------- + atom_array : AtomArray or AtomArrayStack + The nucleic acid structure to measure the dihedral angles for. + For missing backbone atoms the corresponding angles are `NaN`. + + Returns + ------- + alpha, beta, gamma, delta, epsilon, zeta : ndarray, shape=(m,n) or shape=(n,), dtype=float + An array containing the six backbone dihedral angles for every nucleotide + residue. + :math:`\alpha` is not defined at the 5'-terminus, :math:`\epsilon` and + :math:`\zeta` are not defined at the 3'-terminus. + In these places the arrays have *NaN* values. + If an :class:`AtomArrayStack` is given, the output angles are 2-dimensional, + the first dimension corresponds to the model number. + + Notes + ----- + The nucleotide backbone dihedral angles are defined as follows + (indices refer to the residue position along the chain): + + - :math:`\alpha`: O3'(i-1) - P(i) - O5'(i) - C5'(i) + - :math:`\beta`: P(i) - O5'(i) - C5'(i) - C4'(i) + - :math:`\gamma`: O5'(i) - C5'(i) - C4'(i) - C3'(i) + - :math:`\delta`: C5'(i) - C4'(i) - C3'(i) - O3'(i) + - :math:`\epsilon`: C4'(i) - C3'(i) - O3'(i) - P(i+1) + - :math:`\zeta`: C3'(i) - O3'(i) - P(i+1) - O5'(i+1) + """ + coord_p, coord_o5p, coord_c5p, coord_c4p, coord_c3p, coord_o3p = ( + coord_for_atom_name_per_residue( + atom_array, + ("P", "O5'", "C5'", "C4'", "C3'", "O3'"), + filter_nucleotides(atom_array), + ) + ) + n_residues = coord_p.shape[-2] + + # Coordinates for dihedral angle calculation + # Dim 0: Model index (only for atom array stacks) + # Dim 1: Angle index + # Dim 2: X, Y, Z coordinates + # Dim 3: Atoms involved in dihedral angle + if isinstance(atom_array, AtomArray): + angle_coord_shape: tuple[int, ...] = (n_residues, 3, 4) + elif isinstance(atom_array, AtomArrayStack): + angle_coord_shape = (atom_array.stack_depth(), n_residues, 3, 4) + ( + coord_for_alpha, + coord_for_beta, + coord_for_gamma, + coord_for_delta, + coord_for_epsilon, + coord_for_zeta, + ) = [np.full(angle_coord_shape, np.nan, dtype=np.float32) for _ in range(6)] + + # fmt: off + coord_for_alpha[..., 1:, :, 0] = coord_o3p[..., 0:-1, :] + coord_for_alpha[..., 1:, :, 1] = coord_p[..., 1:, :] + coord_for_alpha[..., 1:, :, 2] = coord_o5p[..., 1:, :] + coord_for_alpha[..., 1:, :, 3] = coord_c5p[..., 1:, :] + + coord_for_beta[..., :, :, 0] = coord_p + coord_for_beta[..., :, :, 1] = coord_o5p + coord_for_beta[..., :, :, 2] = coord_c5p + coord_for_beta[..., :, :, 3] = coord_c4p + + coord_for_gamma[..., :, :, 0] = coord_o5p + coord_for_gamma[..., :, :, 1] = coord_c5p + coord_for_gamma[..., :, :, 2] = coord_c4p + coord_for_gamma[..., :, :, 3] = coord_c3p + + coord_for_delta[..., :, :, 0] = coord_c5p + coord_for_delta[..., :, :, 1] = coord_c4p + coord_for_delta[..., :, :, 2] = coord_c3p + coord_for_delta[..., :, :, 3] = coord_o3p + + coord_for_epsilon[..., 0:-1, :, 0] = coord_c4p[..., 0:-1, :] + coord_for_epsilon[..., 0:-1, :, 1] = coord_c3p[..., 0:-1, :] + coord_for_epsilon[..., 0:-1, :, 2] = coord_o3p[..., 0:-1, :] + coord_for_epsilon[..., 0:-1, :, 3] = coord_p[..., 1:, :] + + coord_for_zeta[..., 0:-1, :, 0] = coord_c3p[..., 0:-1, :] + coord_for_zeta[..., 0:-1, :, 1] = coord_o3p[..., 0:-1, :] + coord_for_zeta[..., 0:-1, :, 2] = coord_p[..., 1:, :] + coord_for_zeta[..., 0:-1, :, 3] = coord_o5p[..., 1:, :] + # fmt: on + + alpha = dihedral(*(coord_for_alpha[..., i] for i in range(4))) + beta = dihedral(*(coord_for_beta[..., i] for i in range(4))) + gamma = dihedral(*(coord_for_gamma[..., i] for i in range(4))) + delta = dihedral(*(coord_for_delta[..., i] for i in range(4))) + epsilon = dihedral(*(coord_for_epsilon[..., i] for i in range(4))) + zeta = dihedral(*(coord_for_zeta[..., i] for i in range(4))) + + return alpha, beta, gamma, delta, epsilon, zeta + + +def nucleotide_dihedral_side_chain(atoms): + r""" + Measure the glycosidic :math:`\chi` dihedral angle of nucleotide residues. + + Parameters + ---------- + atoms : AtomArray or AtomArrayStack + The nucleic acid structure to measure the glycosidic dihedral angles for. + + Returns + ------- + chi : ndarray, shape=(m, n) or shape=(n,), dtype=float + An array containing the :math:`\chi` angle for every residue. + Residues that are not nucleotides or lack the required atoms are filled with + :math:`NaN` values. + + Notes + ----- + The :math:`\chi` angle is defined between the sugar and the base: + + - Purines (e.g. ``A``, ``G``): ``O4' - C1' - N9 - C4`` + - Pyrimidines (e.g. ``C``, ``U``, ``T``): ``O4' - C1' - N1 - C2`` + + The base type is inferred from the presence of the ``N9`` atom, so modified + nucleotides are handled as long as they use the canonical glycosidic linkage. + """ + coord_o4p, coord_c1p, coord_n9, coord_c4, coord_n1, coord_c2 = ( + coord_for_atom_name_per_residue( + atoms, + ("O4'", "C1'", "N9", "C4", "N1", "C2"), + filter_nucleotides(atoms), + ) + ) + + purine_chi = dihedral(coord_o4p, coord_c1p, coord_n9, coord_c4) + pyrimidine_chi = dihedral(coord_o4p, coord_c1p, coord_n1, coord_c2) + # Purines are distinguished from pyrimidines by the presence of the N9 atom + is_pyrimidine = np.isnan(coord_n9[..., 0]) + return np.where(is_pyrimidine, pyrimidine_chi, purine_chi) + + def centroid(atoms): """ Measure the centroid of a structure. diff --git a/tests/structure/data/misc/dihedrals.py b/tests/structure/data/misc/dihedrals.py index 5e499140c..9d2cc5338 100644 --- a/tests/structure/data/misc/dihedrals.py +++ b/tests/structure/data/misc/dihedrals.py @@ -28,7 +28,7 @@ # MDTraj only outputs the dihedral angles only for residues, # where they are applicable # -> Map the angles to the correct residues using the returned indices - # amd keep the inapplicable residues as NaN + # and keep the inapplicable residues as NaN mapped_dihedrals = np.full((struc.get_residue_count(atoms)), np.nan) # Use the second atom of each angle to infer the residue, # to handle the edge case of 'phi' diff --git a/tests/structure/data/misc/nucleotide_dihedrals.json b/tests/structure/data/misc/nucleotide_dihedrals.json new file mode 100644 index 000000000..4f6b54c17 --- /dev/null +++ b/tests/structure/data/misc/nucleotide_dihedrals.json @@ -0,0 +1,597 @@ +{ + "alpha": [ + NaN, + 3.129178762435913, + -1.3167532682418823, + -0.9716479778289795, + -1.1392117738723755, + -0.9465111494064331, + -1.1149863004684448, + -1.051683783531189, + 3.019638776779175, + -0.8192322850227356, + -1.0631462335586548, + -1.6773056983947754, + -1.0624338388442993, + -1.3574143648147583, + -0.9876025319099426, + -1.0532798767089844, + -1.1509069204330444, + -1.2893946170806885, + -0.9175159335136414, + -1.0654815435409546, + -1.1407829523086548, + -0.9069551229476929, + -1.052749514579773, + -1.0811176300048828, + -1.0798956155776978, + 3.086649179458618, + 1.1585880517959595, + 3.0220351219177246, + -1.200608491897583, + -1.7492223978042603, + -0.883877158164978, + -1.0395970344543457, + -1.026797890663147, + -0.8619377613067627, + -0.9691030979156494, + -1.0962483882904053, + -0.9425375461578369, + -1.2035574913024902, + -1.1121994256973267, + -0.9712004661560059, + -0.9068886041641235, + -0.9224209785461426, + -1.313096046447754, + -1.1225318908691406, + -1.2512058019638062, + -0.8597368001937866, + -1.1651016473770142, + -1.105143666267395, + 2.9159159660339355, + -0.8929950594902039, + -1.1056554317474365, + 1.6230762004852295, + -1.1327852010726929, + -1.5582321882247925, + -1.1084922552108765, + -0.850636899471283, + -1.056770920753479, + -0.9830983877182007, + -0.913496732711792, + -1.0967696905136108, + -1.1055781841278076, + -1.08992338180542, + 1.3476128578186035, + -1.340139389038086, + 0.96597820520401, + -1.0292452573776245, + -1.2892520427703857, + -0.985615611076355, + -1.0647894144058228, + -1.4197356700897217, + -0.9210435152053833, + -1.0870870351791382, + -1.227248191833496, + 1.4946215152740479, + -1.0194809436798096, + -1.1385815143585205, + -1.7605698108673096, + -0.7452796697616577, + -1.3147536516189575, + -0.9841200113296509, + -1.4469691514968872, + -1.2277984619140625, + 3.011624336242676 + ], + "beta": [ + -2.588057041168213, + -2.5737757682800293, + 2.803341865539551, + 2.977226495742798, + 2.881235122680664, + 2.931342124938965, + 2.9286322593688965, + 2.995654821395874, + 3.097679615020752, + 2.4835453033447266, + 2.710102081298828, + 2.1773478984832764, + -2.5468616485595703, + 2.716113805770874, + 3.009114980697632, + 3.035712957382202, + 3.0792930126190186, + 2.8752546310424805, + 3.033106803894043, + 2.943852424621582, + 2.9083638191223145, + 2.882737159729004, + 2.8034722805023193, + 3.0244622230529785, + 3.0419681072235107, + 2.554131031036377, + -2.7517457008361816, + 3.0309667587280273, + 2.933285713195801, + 3.129457712173462, + 2.8808887004852295, + 2.969789505004883, + 2.844766616821289, + 2.838963508605957, + 2.9209587574005127, + 2.995784044265747, + 2.9622228145599365, + 3.0778253078460693, + 2.9838876724243164, + 3.093174695968628, + 2.845712661743164, + 2.90970778465271, + -2.581507682800293, + -3.1385600566864014, + 3.0319719314575195, + 2.8741698265075684, + 3.0761585235595703, + 2.788128137588501, + 2.6486966609954834, + 2.9033315181732178, + -2.6584889888763428, + -2.760131359100342, + 3.11547589302063, + 3.0754683017730713, + 3.1383938789367676, + 2.990285873413086, + 3.0296568870544434, + 3.008418560028076, + 2.786850690841675, + 3.036226511001587, + -3.050961971282959, + -3.0518951416015625, + -2.793750762939453, + 2.8210413455963135, + -2.3167779445648193, + 2.9503884315490723, + -3.130268096923828, + 3.0088117122650146, + 2.8115787506103516, + 2.899506092071533, + 2.7737035751342773, + -3.07858943939209, + 2.901801109313965, + -3.080864667892456, + -2.854923963546753, + -1.3357088565826416, + 2.115987539291382, + 2.927964687347412, + 2.9489691257476807, + 2.9948432445526123, + 2.794888734817505, + 2.588502883911133, + 2.8838891983032227 + ], + "gamma": [ + 1.1222330331802368, + 2.2407495975494385, + 0.7838222980499268, + 0.7876975536346436, + 1.038230299949646, + 0.8889237642288208, + 1.015411615371704, + 1.012320637702942, + -1.2055388689041138, + 0.87614506483078, + 0.7022848129272461, + 1.4943122863769531, + 1.0915542840957642, + 0.6660923361778259, + 0.7671563029289246, + 0.8910592794418335, + 0.9261267781257629, + 1.1068428754806519, + 0.7565873861312866, + 0.9365060329437256, + 0.9691243767738342, + 0.7246387004852295, + 1.0242584943771362, + 0.9937206506729126, + 0.8806059956550598, + 0.6402716040611267, + -2.8399739265441895, + 0.73869788646698, + 0.9704504609107971, + 1.497573733329773, + 0.8867976665496826, + 1.0977236032485962, + 0.960029125213623, + 0.8060354590415955, + 0.8957772850990295, + 0.9555630087852478, + 0.8233678340911865, + 0.9502443075180054, + 1.0555070638656616, + 0.9262887239456177, + 0.922794759273529, + 1.1741178035736084, + 0.5653551816940308, + 0.9047569632530212, + 1.0605860948562622, + 0.9874451756477356, + 0.9115180373191833, + 0.8936984539031982, + 1.0506631135940552, + 0.8703606128692627, + 0.9727460145950317, + 1.0901391506195068, + 1.0519167184829712, + 0.69178307056427, + 0.8670129776000977, + 0.7500718235969543, + 0.9309889078140259, + 0.8299952745437622, + 0.905141294002533, + 0.9012371301651001, + 0.8772910833358765, + 0.9801585078239441, + 0.9166737198829651, + 0.8453925251960754, + -2.6787631511688232, + 0.788424551486969, + 0.9163419008255005, + 0.8609037399291992, + 1.0044503211975098, + 1.4074312448501587, + 0.9698102474212646, + 1.1518418788909912, + 0.585623025894165, + 1.126158356666565, + 0.8950002789497375, + -1.2974553108215332, + 1.6177881956100464, + 0.669147253036499, + 1.1420215368270874, + 0.8905043601989746, + 1.5575677156448364, + 1.0204745531082153, + 0.9482663869857788 + ], + "delta": [ + 2.301959991455078, + 1.3991398811340332, + 1.467225432395935, + 1.372703194618225, + 1.3219196796417236, + 1.4262850284576416, + 1.3976134061813354, + 1.3324617147445679, + 2.64693546295166, + 2.5841281414031982, + 2.653778076171875, + 1.81106698513031, + 2.7519147396087646, + 1.4347482919692993, + 1.3812247514724731, + 1.336404800415039, + 1.3708431720733643, + 1.3775678873062134, + 1.4093540906906128, + 1.4350523948669434, + 1.4299966096878052, + 1.38666570186615, + 1.3453150987625122, + 1.40914785861969, + 1.4273065328598022, + 2.6242051124572754, + 2.5920448303222656, + 1.4885152578353882, + 1.4467718601226807, + 1.2599763870239258, + 1.357060194015503, + 1.3195040225982666, + 1.3696496486663818, + 1.4074461460113525, + 1.3598545789718628, + 1.345329761505127, + 1.3230210542678833, + 1.3853881359100342, + 1.4635895490646362, + 1.4111711978912354, + 1.4966709613800049, + 2.6492888927459717, + 1.3325283527374268, + 1.4061410427093506, + 1.3820171356201172, + 1.3712427616119385, + 1.4112069606781006, + 1.4975632429122925, + 1.4112509489059448, + 1.5411421060562134, + 2.4539389610290527, + 1.4081472158432007, + 2.50852632522583, + 1.3729557991027832, + 1.4066327810287476, + 1.403278112411499, + 1.453322172164917, + 1.4371744394302368, + 1.4411218166351318, + 1.4266082048416138, + 1.3880493640899658, + 2.540188789367676, + 2.682210683822632, + 2.566222906112671, + 1.4660927057266235, + 1.354446291923523, + 1.3759721517562866, + 1.3290609121322632, + 1.4233378171920776, + 1.2344474792480469, + 1.4872187376022339, + 2.6372783184051514, + 2.539311408996582, + 2.570134401321411, + 1.571976900100708, + 2.799466371536255, + 1.1455061435699463, + 1.3138402700424194, + 1.3557816743850708, + 1.4160836935043335, + 1.465451955795288, + 1.441266417503357, + 1.497050404548645 + ], + "epsilon": [ + 0.6107903122901917, + -2.3486697673797607, + -2.44706392288208, + -2.6325793266296387, + -2.6777758598327637, + -2.5993149280548096, + -2.669710636138916, + -2.659101963043213, + -1.8404031991958618, + -2.558634042739868, + -1.2878056764602661, + -1.8113406896591187, + -2.293726921081543, + -2.5709431171417236, + -2.743091106414795, + -2.7559585571289062, + -2.5708372592926025, + -2.6069092750549316, + -2.625201463699341, + -2.558720827102661, + -2.465200662612915, + -2.500795364379883, + -2.7239949703216553, + -2.6512138843536377, + -2.7452969551086426, + -1.589720606803894, + -1.750584363937378, + -2.5147104263305664, + -2.819106101989746, + -2.6597273349761963, + -2.445855140686035, + -2.477898359298706, + -2.559098243713379, + -2.5767271518707275, + -2.6155130863189697, + -2.6383962631225586, + -2.711751937866211, + -2.57658052444458, + -2.303783893585205, + -2.4307849407196045, + -2.709613084793091, + -2.3822245597839355, + -2.755446434020996, + -2.6314334869384766, + -2.6618642807006836, + -2.6055119037628174, + -2.146799087524414, + -2.549856424331665, + -2.258641004562378, + -2.475766658782959, + -1.6825958490371704, + -2.54272723197937, + -1.630706548690796, + -2.6535468101501465, + -2.578333616256714, + -2.7011661529541016, + -2.6507256031036377, + -2.616518974304199, + -2.6234233379364014, + -2.2635552883148193, + -2.4843626022338867, + -1.7115890979766846, + -1.6911453008651733, + -2.1137216091156006, + -2.4429287910461426, + -2.7436835765838623, + -2.6048736572265625, + -2.5604753494262695, + -2.722506284713745, + -2.457623243331909, + -2.6945338249206543, + -1.7128411531448364, + -2.0143513679504395, + -1.6875282526016235, + -1.9430662393569946, + -2.658823013305664, + -2.664055824279785, + -2.7111642360687256, + -2.7060048580169678, + -2.614166021347046, + -2.1639628410339355, + -2.3961398601531982, + NaN + ], + "zeta": [ + -1.3586524724960327, + 2.4474844932556152, + -1.396604061126709, + -1.2353615760803223, + -1.386718988418579, + -1.3027980327606201, + -1.3206870555877686, + -1.0292671918869019, + -2.328282594680786, + 2.290024757385254, + 2.0840957164764404, + 2.5559372901916504, + 1.8314509391784668, + -1.3342828750610352, + -1.20218825340271, + -1.175121784210205, + -1.186467170715332, + -1.2486168146133423, + -1.1478567123413086, + -1.3641242980957031, + -1.3829302787780762, + -1.400326132774353, + -1.2338451147079468, + -1.263436198234558, + 1.0130748748779297, + 1.5061947107315063, + -1.1584324836730957, + -1.2428765296936035, + -1.226115107536316, + -1.3939155340194702, + -1.2157047986984253, + -1.1011236906051636, + -1.4382505416870117, + -1.2429189682006836, + -1.267744541168213, + -1.3679219484329224, + -1.0909796953201294, + -1.2198314666748047, + 2.5084238052368164, + -1.2543842792510986, + -1.3370559215545654, + -3.0301127433776855, + -1.271095871925354, + -1.1359769105911255, + -1.3209574222564697, + -1.1966595649719238, + -1.28355872631073, + -1.1503582000732422, + -1.3394718170166016, + -1.1155942678451538, + 3.1156365871429443, + -0.9276810884475708, + -1.2480039596557617, + -1.3507370948791504, + -1.39621901512146, + -1.2325828075408936, + -1.290710210800171, + -1.2937495708465576, + -1.2656776905059814, + -1.261549949645996, + -1.1846519708633423, + -1.7667421102523804, + -1.5601160526275635, + -2.8413889408111572, + -1.315072774887085, + -1.082815408706665, + -1.2081671953201294, + -1.3559510707855225, + -1.0487524271011353, + -1.4536592960357666, + -1.3268591165542603, + 2.5784380435943604, + 1.2020344734191895, + -1.1822447776794434, + -1.2636638879776, + -1.891784429550171, + -1.4695253372192383, + -1.2115700244903564, + -1.2117794752120972, + -1.0827745199203491, + -0.9297324419021606, + -1.1077589988708496, + NaN + ], + "chi": [ + -2.4983904361724854, + 1.1298459768295288, + -2.971721649169922, + -2.7846391201019287, + -2.7500245571136475, + -2.7860021591186523, + -2.7713167667388916, + -2.6717278957366943, + 0.984861433506012, + -1.6530272960662842, + -2.5353009700775146, + -1.6746824979782104, + -1.7182544469833374, + -2.9703385829925537, + -2.7890729904174805, + -2.85819935798645, + -2.785080909729004, + -3.0230300426483154, + -2.8812806606292725, + -2.7709505558013916, + -2.8624534606933594, + -2.8339641094207764, + -2.934645891189575, + -2.826885461807251, + -2.6289312839508057, + -2.26149845123291, + -1.3264024257659912, + -3.0244383811950684, + -2.855647325515747, + -2.901296377182007, + -3.0002427101135254, + -3.0347940921783447, + -2.9397850036621094, + -2.8365981578826904, + -2.7801215648651123, + -2.7819578647613525, + -2.788468837738037, + -2.8409337997436523, + -2.674703598022461, + -2.8691070079803467, + -2.7735211849212646, + -2.250523090362549, + -3.025674343109131, + -2.8545093536376953, + -2.9028148651123047, + -2.8979477882385254, + -2.6750540733337402, + -2.6482906341552734, + -2.9359514713287354, + -2.5351901054382324, + -1.460233449935913, + -3.0368354320526123, + -2.1283462047576904, + -2.9940743446350098, + -2.822737455368042, + -2.8391165733337402, + -2.861114263534546, + -2.7746686935424805, + -2.7987823486328125, + -2.729644536972046, + -2.776649236679077, + -2.192107677459717, + -2.723100185394287, + -1.6294564008712769, + -2.788125991821289, + -2.798078775405884, + -2.757357597351074, + -2.8073570728302, + -2.8287267684936523, + -3.110689163208008, + -3.0244970321655273, + -2.22491192817688, + -2.29485821723938, + -2.3116884231567383, + -2.3839542865753174, + -1.6798408031463623, + -2.793205738067627, + -2.754098653793335, + -2.8391568660736084, + -2.708035945892334, + -2.7798757553100586, + -2.7182841300964355, + -2.9050567150115967 + ] +} \ No newline at end of file diff --git a/tests/structure/data/misc/nucleotide_dihedrals.py b/tests/structure/data/misc/nucleotide_dihedrals.py new file mode 100644 index 000000000..1f41a1cd1 --- /dev/null +++ b/tests/structure/data/misc/nucleotide_dihedrals.py @@ -0,0 +1,24 @@ +import json +from pathlib import Path +import barnaba + +# `barnaba` does not properly handle discontinuities in the structure +# -> A structure without discontinuities is selected +STRUCTURE_FILE = Path(__file__).parents[1] / "4p5j.cif" +OUTPUT_FILE = Path(__file__).parent / "nucleotide_dihedrals.json" + +# Order of angles in the array returned by `barnaba.backbone_angles()` +ANGLE_NAMES = ["alpha", "beta", "gamma", "delta", "epsilon", "zeta", "chi"] + + +if __name__ == "__main__": + # `barnaba` returns angles only for canonical nucleotides, in the order they + # appear in the structure. The single model of the structure is used. + ba_angles, _ = barnaba.backbone_angles(STRUCTURE_FILE.as_posix()) + ba_angles = ba_angles[0] + + dihedral_dict = { + name: ba_angles[:, i].tolist() for i, name in enumerate(ANGLE_NAMES) + } + with open(OUTPUT_FILE, "w") as file: + json.dump(dihedral_dict, file, indent=4) diff --git a/tests/structure/test_geometry.py b/tests/structure/test_geometry.py index 4cb38dab9..d7ff1c003 100644 --- a/tests/structure/test_geometry.py +++ b/tests/structure/test_geometry.py @@ -130,6 +130,64 @@ def test_dihedral_side_chain_consistency(multi_model): assert test_chi == pytest.approx(ref_chi, abs=1e-3, nan_ok=True) +@pytest.mark.parametrize("multi_model", [False, True]) +def test_nucleotide_dihedral_consistency(multi_model): + """ + Check if the computed nucleotide dihedral angles via + :func:`nucleotide_dihedral_backbone()` and :func:`nucleotide_dihedral_side_chain()` + are equal to the reference computed with ``barnaba``. + """ + with open(join(data_dir("structure"), "misc", "nucleotide_dihedrals.json")) as file: + ref_dihedrals = json.load(file) + + pdbx_file = pdbx.BinaryCIFFile.read(join(data_dir("structure"), "4p5j.bcif")) + atoms = pdbx.get_structure(pdbx_file, model=1) + if multi_model: + atoms = struc.stack([atoms] * 2) + # `barnaba` only outputs values for canonical nucleotides + # -> filter them here as well for consistency + atoms = atoms[..., struc.filter_canonical_nucleotides(atoms)] + + test_bb_angles = struc.nucleotide_dihedral_backbone(atoms) + test_chi = struc.nucleotide_dihedral_side_chain(atoms) + test_angles = test_bb_angles + (test_chi,) + + if multi_model: + for angle in test_angles: + assert np.array_equal(angle[1], angle[0], equal_nan=True) + test_angles = tuple(angle[0] for angle in test_angles) + + for angle_name, test_angle in zip( + ["alpha", "beta", "gamma", "delta", "epsilon", "zeta", "chi"], test_angles + ): + ref_angle = np.array(ref_dihedrals[angle_name]) + assert test_angle == pytest.approx(ref_angle, abs=1e-3, nan_ok=True) + + +def test_nucleotide_dihedrals_full_structure(): + """ + :func:`nucleotide_dihedral_backbone()` and :func:`nucleotide_dihedral_side_chain()` + should be able to run on structures with non-canonical nucleotides. + For non-nucleotide residues all angles should be NaN. + """ + # `4p5j` contains a non-canonical nucleotide at the terminus + pdbx_file = pdbx.BinaryCIFFile.read(join(data_dir("structure"), "4p5j.bcif")) + atoms = pdbx.get_structure(pdbx_file, model=1) + # This mask will also include the non-canonical nucleotide + nucleotide_mask = struc.filter_nucleotides(atoms[struc.get_residue_starts(atoms)]) + + bb_angles = np.stack(struc.nucleotide_dihedral_backbone(atoms), axis=-1) + chi_angles = struc.nucleotide_dihedral_side_chain(atoms) + + # For nucleotide residues the dihedral angles should be defined + # (with exception of some backbone angles, as they are missing at the termini) + assert np.all(np.isfinite(bb_angles[nucleotide_mask]).any(axis=-1)) + assert np.all(np.isfinite(chi_angles[nucleotide_mask])) + # For non-nucleotide residues all angles should be NaN + assert np.all(np.isnan(bb_angles[~nucleotide_mask])) + assert np.all(np.isnan(chi_angles[~nucleotide_mask])) + + def test_index_distance_non_periodic(): """ Without PBC the result should be equal to the normal distance