#!/usr/bin/env python

"""
Typical Usage: 

coords2dihedrals.py 180 < 12column_text_file.dat

This script reads a 12-column numeric text file and computes dihedral angles
(and other inclusive angles and distances).  Example input file:

30.13 11.46 15.12 35.28 -2.32 12.61 30.68 -4.45 16.30 28.68 8.46 10.24
28.68 8.42 10.24 30.68 -4.45 16.30 25.22 -4.75 19.42 27.57 3.37 8.06
27.57 3.37 8.06 25.22 -4.75 19.42 20.45 -1.27 20.09 25.80 -2.26 6.61
25.80 -2.23 6.61 20.45 -1.27 20.09 15.47 1.36 18.12 21.67 -5.88 7.17
:
The 12 numbers on each line represent the x,y,z coordinates of 4 atoms.
(This is the format of the files generated by the "pdb2coords.py" script.)
For each line, the program calculates the (4-body) dihedral angle for 
these 4 atoms, followed by the (3-body) bond angle between atoms 1,2,3 and 2,3,4
followed by distances between atoms 1,2 and 2,3 and 3,4, and writes them
(in that order) to the standard out.  (6 numbers total, followed by a newline)
(When a line contains the wrong number of numbers, 
 the script prints out a list of 6 impossibe negative values:
 "-720 -360 -360 -1 -1 -1" to let the caller know that 
 this particular angle could not be computed.)

Note:   
The "IUPAC/IUB" dihedral-angle convention is used:
4 atoms in the "trans" conformation have a dihedral angle of 180 degrees.
By default, dihedral angles are calculated in the range from 0 to 360.0 degrees.
(This means there is discontinuity in the angle at 0 degrees.)
However this may be a bad choice for polymers which can alternate between
helical conformations which are left and right handed.
In some cases, you may want to have the discontinuity appear at 180 degrees.
(or some other angle which is sparsely populated).

So you can supply an optional argument (the "branch_of_log") which indicates 
where the discontinuity in the dihedral angle will appear.
Dihedral angles returned by this program will lie in the range: 
[branch_of_log-360, branch_of_log)

"""

import sys
from math import sqrt, cos, sin, tan, acos, asin, atan, pi, floor
# Sometimes this program pipes its output to other programs which halt early.
# Below we silently suppress the ugly "Broken pipe" message this generates:
import signal
signal.signal(signal.SIGPIPE, signal.SIG_DFL)


def length_v(r):
    lsqd = 0.0
    for d in range(0,len(r)):
        lsqd += r[d]*r[d]
    return sqrt(lsqd)


def inner_prod_v(r1,r2):
    result = 0.0
    for d in range(0,len(r1)):
        result += r1[d]*r2[d]
    return result


def cross_prod_v3(a,b):
    c = [0.0,0.0,0.0]
    c[0] = a[1]*b[2] - a[2]*b[1]
    c[1] = a[2]*b[0] - a[0]*b[2]
    c[2] = a[0]*b[1] - a[1]*b[0]
    return c



def main():

    branch_of_log = 2.0*pi  # by default, dihedral angles lie in range: [0, 360.0)
    truncate_a    = 0
    truncate_b    = 0

    if (len(sys.argv) > 4):
        sys.stderr.write('Error (coords2dihedrals): number of arguments should not exceed 3.\n'\
                         '    If an odd-number of arguments are passed (1 or 3), then\n'
                         '    the first argument is assumed to be the branch-of-log, a number which is\n'
                         '    0 and 360.0.  This argument indicates where the discontinuity in the\n'
                         '    dihedral angle is located.  By default it is 0 degrees, which\n'
                         '    corresponds to 4 atoms in the "cis" conformation.)\n'
                         '   (The two arguments correspond to the number of lines of\n'\
                         '     text to omit from the beginning and end of the file, respectively.)\n'\
                         '     If one argument is passed, then both are assumed to be the same.\n'\
                         '     If no argument is passed, then by default, no data is ignored.\nExiting...\n\n')
        sys.exit(-1)

    # NOTE: The "truncate" arguments are not really supported any more.  Instead
    #       use other scripts to post-process the results printed by this program.
    elif (len(sys.argv) == 4):
        branch_of_log = float(sys.argv[1])
        truncate_a    =   int(sys.argv[2])
        truncate_b    =   int(sys.argv[3])
    elif (len(sys.argv) == 3):
        truncate_a = int(sys.argv[1])
        truncate_b = int(sys.argv[2])
    elif (len(sys.argv) == 2):
        branch_of_log = float(sys.argv[1])
        branch_of_log *= pi/180.0


    coords_list = []
    # Read the file
    for line in sys.stdin:
        line = line.strip()
        # Each line should contain a list of 3 numbers separated by whitespace.
        # If so, store the 3 numbers in a list variable (named xyz), and append
        # it to the list of coordinates.
        # However some lines might also be blank, in which case we append the
        # empty list [] to the list of coordinates.
        if line == '':
            coords = []
        else:
            # Each line should contain a list of 3 numbers separated by whitespace.
            coords = list(map(float, line.split()))
            if len(coords) != 4*3:
                sys.stderr.write('Error(coords2dihedrals):\n'+'Each line should either contain 12 numbers or be blank.\n')
                sys.exit(-1)
        coords_list.append(coords)


    # Truncate the data we don't want.
    # (Why?  The residues at the beginning and ending of helices 
    #  are less trustworthy then the residues in the middle.)
    coords_list = coords_list[truncate_a:len(coords_list)-truncate_b]

    N = len(coords_list)
    for i in range(0,N):
        if len(coords_list[i]) == 3*4:
            r10 = [0.0, 0.0, 0.0]
            r21 = [0.0, 0.0, 0.0]
            r32 = [0.0, 0.0, 0.0]
            for d in range(0,3):
                r10[d] = coords_list[i][3*1+d] - coords_list[i][3*0+d]
                r21[d] = coords_list[i][3*2+d] - coords_list[i][3*1+d]
                r32[d] = coords_list[i][3*3+d] - coords_list[i][3*2+d]
            l10 = length_v(r10)
            l21 = length_v(r21)
            l32 = length_v(r32)

            n012 = cross_prod_v3(r10, r21)
            n123 = cross_prod_v3(r21, r32)

            # The dihedral-angle or 4-body angle is named "angle0124"
            cos_phi = inner_prod_v(n012, n123) /(length_v(n012)*length_v(n123))

            # There is a problem whenever 4 consecutive atoms are coplanar:
            #
            #            *---*
            #                |      (all 4 atoms are coplanar, and phi = 0)
            #            *---*
            #
            # In this example, the dihedral angle phi is well defined and =0.
            # The problem is that, due to floating point roundoff
            # "cos_phi" can sometimes slightly exceed 1.
            # This causes a NAN when you calculate acos(cos_phi).

            if (cos_phi > 1.0):
                cos_phi = 1.0
            elif (cos_phi < -1.0):
                cos_phi = -1.0

            phi = acos(cos_phi)

            # This formula does not distinguish positive and negative phi.
            #
            # Negative dihedral angles:
            #
            # Check if  the position of atom i+3 is above the phi=0 plane
            # (in the region of positive phi), or below the phi=0 plane.
            # It is above the phi=0 plane if the bond from atom i+2 to i+3
            # points in the same direction as the negative-phi-tangent-vector 
            # for atom i (not i+3)  (...which points in the n012 direction)
            if inner_prod_v(n012, r32) < 0.0:
                phi = -phi

            phi_range_a = branch_of_log - (2.0*pi)
            phi_range_b = branch_of_log
            nphi = floor((phi - phi_range_a) / (2.0*pi))
            phi = phi - (nphi*2.0*pi)

            theta0 = length_v(n012) / (l10*l21)
            theta1 = length_v(n123) / (l21*l32)

            sys.stdout.write(str(phi*180.0/pi) + ' ' +
                             str(theta0*180.0/pi) + ' ' +
                             str(theta1*180.0/pi) + ' ' +
                             str(l10) + ' ' +
                             str(l21) + ' ' +
                             str(l32) +
                             '\n')
        else:
            # Otherwise, we write out an impossible values to let the caller 
            # know that this particular dihedral angle could not be computed
            sys.stdout.write('-720 -360 -360 -1 -1 -1\n')


if __name__ == "__main__":
    main()
