Skip to content

Commit ea51e2e

Browse files
authored
Add chiangles (#67)
This computes the standard `chi` angles for standard residues. The table was taken from http://www.mlb.co.jp/linux/science/garlic/doc/commands/dihedrals.html Cases where there are 180° symmetries in atom-naming are handled by ensuring that the corresponding angle is positive.
1 parent a902b4e commit ea51e2e

File tree

3 files changed

+200
-1
lines changed

3 files changed

+200
-1
lines changed

docs/src/documentation.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,9 +411,11 @@ Various functions are provided to calculate spatial quantities for proteins:
411411
| [`omegaangle`](@ref) | Omega dihedral angle between a residue and the previous residue |
412412
| [`phiangle`](@ref) | Phi dihedral angle between a residue and the previous residue |
413413
| [`psiangle`](@ref) | Psi dihedral angle between a residue and the next residue |
414+
| [`chiangle`](@ref) | Chi dihedral angle within a residue |
414415
| [`omegaangles`](@ref) | `Vector` of omega dihedral angles of an element |
415416
| [`phiangles`](@ref) | `Vector` of phi dihedral angles of an element |
416417
| [`psiangles`](@ref) | `Vector` of psi dihedral angles of an element |
418+
| [`chiangles`](@ref) | All chi dihedral angles for a residue or element |
417419
| [`ramachandranangles`](@ref) | `Vector`s of phi and psi angles of an element |
418420
| [`ContactMap`](@ref) | `ContactMap` of two elements, or one element with itself |
419421
| [`DistanceMap`](@ref) | `DistanceMap` of two elements, or one element with itself |
@@ -446,6 +448,17 @@ julia> rad2deg(psiangle(struc['A'][50], struc['A'][51]))
446448

447449
julia> rad2deg(psiangle(struc['A'], 50))
448450
-177.38288114072924
451+
452+
julia> rad2deg.(chiangles(struc['A'][2])) # ASP, χ₁...χ₅
453+
5-element Vector{Float64}:
454+
-55.5708140556466
455+
-118.03264320054458
456+
56.50403552503829
457+
-176.37208357380604
458+
0.04991054795811607
459+
460+
julia> rad2deg.(chiangles(struc['A'][4])) # GLY, no χ angles
461+
Float64[]
449462
```
450463

451464
[`ContactMap`](@ref) takes in a structural element or a list, such as a `Chain` or `Vector{Atom}`, and returns a [`ContactMap`](@ref) object showing the contacts between the elements for a specified distance.

src/spatial.jl

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ export
1616
omegaangles,
1717
phiangles,
1818
psiangles,
19+
chiangle,
20+
chiangles,
1921
ramachandranangles,
2022
SpatialMap,
2123
ContactMap,
@@ -341,7 +343,7 @@ function dihedralangle(vec_a::AbstractVector{<:Real},
341343
vec_b::AbstractVector{<:Real},
342344
vec_c::AbstractVector{<:Real})
343345
return atan(
344-
dot(cross(cross(vec_a, vec_b), cross(vec_b, vec_c)), vec_b / norm(vec_b)),
346+
norm(vec_b) * dot(vec_a, cross(vec_b, vec_c)),
345347
dot(cross(vec_a, vec_b), cross(vec_b, vec_c)))
346348
end
347349

@@ -462,6 +464,126 @@ function psiangle(chain::Chain, res_id::Union{Integer, AbstractString})
462464
return psiangle(chain[resids(chain)[i]], chain[resids(chain)[i + 1]])
463465
end
464466

467+
# source: http://www.mlb.co.jp/linux/science/garlic/doc/commands/dihedrals.html
468+
const chitables = [
469+
Dict{String,NTuple{4,String}}(
470+
"ARG" => ("N", "CA", "CB", "CG"),
471+
"ASN" => ("N", "CA", "CB", "CG"),
472+
"ASP" => ("N", "CA", "CB", "CG"),
473+
"CYS" => ("N", "CA", "CB", "SG"),
474+
"GLN" => ("N", "CA", "CB", "CG"),
475+
"GLU" => ("N", "CA", "CB", "CG"),
476+
"HIS" => ("N", "CA", "CB", "CG"),
477+
"ILE" => ("N", "CA", "CB", "CG1"),
478+
"LEU" => ("N", "CA", "CB", "CG"),
479+
"LYS" => ("N", "CA", "CB", "CG"),
480+
"MET" => ("N", "CA", "CB", "CG"),
481+
"PHE" => ("N", "CA", "CB", "CG"),
482+
"PRO" => ("N", "CA", "CB", "CG"),
483+
"SER" => ("N", "CA", "CB", "OG"),
484+
"THR" => ("N", "CA", "CB", "OG1"),
485+
"TRP" => ("N", "CA", "CB", "CG"),
486+
"TYR" => ("N", "CA", "CB", "CG"),
487+
"VAL" => ("N", "CA", "CB", "CG1"),
488+
),
489+
Dict{String,NTuple{4,String}}(
490+
"ARG" => ("CA", "CB", "CG", "CD"),
491+
"ASN" => ("CA", "CB", "CG", "OD1"),
492+
"ASP" => ("CA", "CB", "CG", "OD1"),
493+
"GLN" => ("CA", "CB", "CG", "CD"),
494+
"GLU" => ("CA", "CB", "CG", "CD"),
495+
"HIS" => ("CA", "CB", "CG", "ND1"),
496+
"ILE" => ("CA", "CB", "CG1", "CD1"),
497+
"LEU" => ("CA", "CB", "CG", "CD1"),
498+
"LYS" => ("CA", "CB", "CG", "CD"),
499+
"MET" => ("CA", "CB", "CG", "SD"),
500+
"PHE" => ("CA", "CB", "CG", "CD1"),
501+
"PRO" => ("CA", "CB", "CG", "CD"),
502+
"TRP" => ("CA", "CB", "CG", "CD1"),
503+
"TYR" => ("CA", "CB", "CG", "CD1"),
504+
),
505+
Dict{String,NTuple{4,String}}(
506+
"ARG" => ("CB", "CG", "CD", "NE"),
507+
"GLN" => ("CB", "CG", "CD", "OE1"),
508+
"GLU" => ("CB", "CG", "CD", "OE1"),
509+
"LYS" => ("CB", "CG", "CD", "CE"),
510+
"MET" => ("CB", "CG", "SD", "CE"),
511+
),
512+
Dict{String,NTuple{4,String}}(
513+
"ARG" => ("CG", "CD", "NE", "CZ"),
514+
"LYS" => ("CG", "CD", "CE", "NZ"),
515+
),
516+
Dict{String,NTuple{4,String}}(
517+
"ARG" => ("CD", "NE", "CZ", "NH1"),
518+
),
519+
]
520+
521+
# source: Jumper et al 2021, Supp. Table 2
522+
const chisymmetries = [
523+
("ASP", 2),
524+
("GLU", 3),
525+
("PHE", 2),
526+
("TYR", 2),
527+
]
528+
529+
function chiangle(a1, a2, a3, a4, sym::Bool)
530+
χ = dihedralangle(a1, a2, a3, a4)
531+
if sym && χ < 0
532+
χ += π
533+
end
534+
return χ
535+
end
536+
537+
"""
538+
chiangle(res, i)
539+
540+
Calculate the χᵢ angle in radians for a standard `AbstractResidue` with standard atom names.
541+
The angle is in the range -π to π.
542+
"""
543+
function chiangle(res::AbstractResidue, i::Integer)
544+
1 <= i <= 5 || throw(ArgumentError("χᵢ index `i` must be between 1 and 5"))
545+
ct = chitables[i]
546+
rname = resname(res)
547+
t = get(ct, rname, nothing)
548+
if t === nothing
549+
# is it because this isn't a standard residue?
550+
(rname == "GLY" || rname == "ALA") && throw(ArgumentError("$rname does not have any χ angles"))
551+
haskey(chitables[1], rname) || throw(ArgumentError("no χ angles are defined for residues with name $rname"))
552+
# it must be missing specifically for `i`
553+
j = 1
554+
while haskey(chitables[j+1], rname)
555+
j += 1
556+
end
557+
throw(ArgumentError("χ angle with index $i does not exist for residue $rname (max is $j)"))
558+
end
559+
return chiangle(res[t[1]], res[t[2]], res[t[3]], res[t[4]], (rname, i) in chisymmetries)
560+
end
561+
562+
"""
563+
chiangles(res)
564+
565+
Calculate the `Vector` of standard χ angles for a standard `AbstractResidue` with standard atom names.
566+
The length of the vector ranges from 0 (GLY, ALA) to 5 (ARG).
567+
The angles are each in the range -π to π.
568+
"""
569+
function chiangles(res::AbstractResidue)
570+
chi = Float64[]
571+
rname = resname(res)
572+
for i = 1:5
573+
ct = chitables[i]
574+
t = get(ct, rname, nothing)
575+
if t === nothing
576+
if i == 1
577+
(rname == "GLY" || rname == "ALA") && break
578+
throw(ArgumentError("no χ angles are defined for residues with name $rname"))
579+
end
580+
break
581+
end
582+
push!(chi, chiangle(res[t[1]], res[t[2]], res[t[3]], res[t[4]], (rname, i) in chisymmetries))
583+
end
584+
return chi
585+
end
586+
465587
"""
466588
omegaangles(element, residue_selectors...)
467589
@@ -598,6 +720,17 @@ function ramachandranangles(el::StructuralElementOrList,
598720
return phiangles(el, residue_selectors...), psiangles(el, residue_selectors...)
599721
end
600722

723+
"""
724+
chiangles(element, residue_selectors...)
725+
726+
Calculate the `Vector` of standard χ angles for each residue in a `StructuralElementOrList`.
727+
This returns a `Vector` of `Vector`s, where all angles are in the range -π to π.
728+
Additional arguments are residue selector functions - only residues that return
729+
`true` from the functions are retained.
730+
"""
731+
chiangles(el::StructuralElementOrList, residue_selectors::Function...) =
732+
[chiangles(r) for r in collectresidues(el, residue_selectors...)]
733+
601734
"A map of a structural property, e.g. a `ContactMap` or a `DistanceMap`."
602735
abstract type SpatialMap end
603736

test/runtests.jl

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3191,6 +3191,59 @@ end
31913191
@test isapprox(phis[10], phiangle(struc_1AKE['A'], 10), atol=1e-5)
31923192
@test isapprox(omegas[10], omegaangle(struc_1AKE['A'], 10), atol=1e-5)
31933193

3194+
# Test that the entries in `chitables` are bonded
3195+
sortt((a, b)) = a < b ? (a, b) : (b, a)
3196+
rd = BioStructures.residuedata
3197+
for ct in BioStructures.chitables
3198+
for (rname, alist) in ct
3199+
if rname == "HIS"
3200+
rname = "HID"
3201+
end
3202+
sb = sortt.(rd[rname].bonds)
3203+
for i = 1:3
3204+
@test sortt((alist[i], alist[i+1])) sb
3205+
end
3206+
end
3207+
end
3208+
chis = chiangles(struc_1AKE['A'], standardselector)
3209+
@test length(chis) == countresidues(struc_1AKE['A'], standardselector)
3210+
@test chis[1] deg2rad.([-176.231, 172.056, 56.069]) rtol=1e-5 # MET
3211+
@test chis[2] deg2rad.([-76.551, 171.696, 171.162, -175.969, 0.424]) rtol=1e-5 # ARG
3212+
@test isempty(chis[7]) # GLY
3213+
@test chiangle(struc_1AKE['A'][2], 3) == chis[2][3]
3214+
@test_throws "GLY does not have any χ angles" chiangle(struc_1AKE['A'][7], 1)
3215+
@test_throws "χ angle with index 4 does not exist for residue MET (max is 3)" chiangle(struc_1AKE['A'][1], 4)
3216+
fakeres = Residue("UKN", 10, ' ', false, struc_1AKE['A'])
3217+
@test_throws "no χ angles are defined for residues with name UKN" chiangle(fakeres, 1)
3218+
@test_throws "no χ angles are defined for residues with name UKN" chiangles(fakeres)
3219+
# Test symmetries: two ASPs with OD1 and OD2 swapped
3220+
io = IOBuffer("""
3221+
ATOM 534 N ASP A 1 -8.068 7.150 -2.008 1.00 95.46 N
3222+
ATOM 535 CA ASP A 1 -8.464 6.572 -3.297 1.00 95.46 C
3223+
ATOM 536 C ASP A 1 -8.522 7.636 -4.409 1.00 95.46 C
3224+
ATOM 537 CB ASP A 1 -9.844 5.896 -3.142 1.00 95.46 C
3225+
ATOM 538 O ASP A 1 -8.015 7.431 -5.518 1.00 95.46 O
3226+
ATOM 539 CG ASP A 1 -9.818 4.542 -2.418 1.00 95.46 C
3227+
ATOM 540 OD1 ASP A 1 -8.738 3.930 -2.377 1.00 95.46 O
3228+
ATOM 541 OD2 ASP A 1 -10.899 4.073 -1.981 1.00 95.46 O
3229+
""")
3230+
r = only(only(only(read(io, PDBFormat))))
3231+
χs1 = chiangles(r)
3232+
io = IOBuffer("""
3233+
ATOM 534 N ASP A 1 -8.068 7.150 -2.008 1.00 95.46 N
3234+
ATOM 535 CA ASP A 1 -8.464 6.572 -3.297 1.00 95.46 C
3235+
ATOM 536 C ASP A 1 -8.522 7.636 -4.409 1.00 95.46 C
3236+
ATOM 537 CB ASP A 1 -9.844 5.896 -3.142 1.00 95.46 C
3237+
ATOM 538 O ASP A 1 -8.015 7.431 -5.518 1.00 95.46 O
3238+
ATOM 539 CG ASP A 1 -9.818 4.542 -2.418 1.00 95.46 C
3239+
ATOM 540 OD1 ASP A 1 -10.899 4.073 -1.981 1.00 95.46 O
3240+
ATOM 541 OD2 ASP A 1 -8.738 3.930 -2.377 1.00 95.46 O
3241+
""")
3242+
r = only(only(only(read(io, PDBFormat))))
3243+
χs2 = chiangles(r)
3244+
@test χs1[1] χs2[1]
3245+
@test χs1[2] χs2[2] rtol=0.05 # the bonds are not exactly symmetric
3246+
31943247
# Test ContactMap
31953248
cas = collectatoms(struc_1AKE, calphaselector)[1:10]
31963249
@test isa(ContactMap(cas, 10).data, BitArray{2})

0 commit comments

Comments
 (0)