packages/miew/src/chem/HBondInfo.js

Summary

Maintainability
F
5 days
Test Coverage
import ResidueType from './ResidueType'
import PairCollection from './AtomPairs'

const MINIMAL_DISTANCE = 0.5
const MIN_HBOND_ENERGY = -9.9
const MAX_HBOND_ENERGY = -0.5
const COUPLING_CONSTANT = -27.888 // = -332 * 0.42 * 0.2
const MAX_COUPLING_DISTANCE = 5.0 // how far is the closest atom of a potential partner residue from CA atom
const MAX_RESIDUES_THRESHOLD = 1000

export default class HBondInfo {
  constructor(complex) {
    this._complex = complex
    this._hbonds = [] // array of bond info for each residue
    if (this._complex._residues.length > MAX_RESIDUES_THRESHOLD) {
      this._buildVW() // optimized version using voxel grid
    } else {
      this._build() // test all pairs of residues
    }
  }

  isBond(from, to) {
    if (this._hbonds[from]) {
      const [acc0, acc1] = this._hbonds[from].acceptor
      if (acc0 && acc0.residue === to && acc0.energy < MAX_HBOND_ENERGY) {
        return true
      }
      if (acc1 && acc1.residue === to && acc1.energy < MAX_HBOND_ENERGY) {
        return true
      }
    }
    return false
  }

  _build() {
    const self = this

    for (let i = 0; i < this._complex._residues.length - 1; ++i) {
      const ri = this._complex._residues[i]
      if ((ri.getType().flags & ResidueType.Flags.PROTEIN) === 0) {
        continue
      }

      // get predecessor in chain
      let preri = null
      if (
        i > 0 &&
        this._complex._residues[i - 1].getType().flags &
          ResidueType.Flags.PROTEIN &&
        ri._sequence === this._complex._residues[i - 1]._sequence + 1
      ) {
        preri = this._complex._residues[i - 1]
      }

      for (let j = i + 1; j < this._complex._residues.length; ++j) {
        const rj = this._complex._residues[j]
        if ((rj.getType().flags & ResidueType.Flags.PROTEIN) === 0) {
          continue
        }

        // get predecessor in chain
        let prerj = null
        if (
          this._complex._residues[j - 1].getType().flags &
            ResidueType.Flags.PROTEIN &&
          rj._sequence === this._complex._residues[j - 1]._sequence + 1
        ) {
          prerj = this._complex._residues[j - 1]
        }

        self._calcHBondEnergy(preri, ri, rj)
        if (j !== i + 1) {
          self._calcHBondEnergy(prerj, rj, ri)
        }
      }
    }
  }

  _buildVW() {
    const self = this
    const residues = this._complex._residues
    let ri
    let preri

    const vw = this._complex.getVoxelWorld()
    if (vw === null) {
      return
    }

    const pairs = new PairCollection(
      (this._complex._residues.length * this._complex._residues.length) / 2
    )

    function processAtom(atom) {
      const rj = atom.residue

      if (rj._index === ri._index) {
        return
      }

      if ((rj.getType().flags & ResidueType.Flags.PROTEIN) === 0) {
        return
      }

      if (!pairs.addPair(ri._index, rj._index)) {
        // we've seen this pair
        return
      }

      // get predecessor in chain
      let prerj = rj._index > 0 ? residues[rj._index - 1] : null
      if (
        prerj &&
        ((prerj.getType().flags & ResidueType.Flags.PROTEIN) === 0 ||
          rj._sequence !== prerj._sequence + 1)
      ) {
        prerj = null
      }

      self._calcHBondEnergy(preri, ri, rj)
      if (rj._index !== ri._index + 1) {
        self._calcHBondEnergy(prerj, rj, ri)
      }
    }

    for (let i = 0; i < residues.length - 1; ++i) {
      ri = residues[i]
      if ((ri.getType().flags & ResidueType.Flags.PROTEIN) === 0) {
        continue
      }

      // get predecessor in chain
      preri = i > 0 ? residues[i - 1] : null
      if (
        preri &&
        ((preri.getType().flags & ResidueType.Flags.PROTEIN) === 0 ||
          ri._sequence !== preri._sequence + 1)
      ) {
        preri = null
      }

      vw.forEachAtomWithinRadius(
        this._residueGetCAlpha(ri),
        MAX_COUPLING_DISTANCE,
        processAtom
      )
    }
  }

  _residueGetCAlpha(res) {
    for (let i = 0; i < res._atoms.length; ++i) {
      const { name } = res._atoms[i]
      if (name === 'CA' || name === 'C1') {
        return res._atoms[i].position
      }
    }

    return null
  }

  _residueGetCO(res) {
    let c = null
    let o = null

    res.forEachAtom((a) => {
      if (a.name === 'C') {
        c = a.position
      } else if (a.name === 'O') {
        o = a.position
      }
    })

    return [c, o]
  }

  // TODO Support hydrogen defined in complex
  _residueGetNH(prev, res) {
    const [c, o] = this._residueGetCO(prev)

    let n
    res.forEachAtom((a) => {
      if (a.name === 'N') {
        n = a.position
      }
    })

    if (c && o && n) {
      // calculate hydrogen position
      const h = c.clone()
      h.sub(o)
      h.multiplyScalar(1.0 / h.length())
      h.add(n)

      return [n, h]
    }

    return [null, null]
  }

  _calcHBondEnergy(predonor, donor, acceptor) {
    let result = 0

    if (predonor === null) {
      return result
    }

    if (donor.getType().getName() !== 'PRO') {
      const [n, h] = this._residueGetNH(predonor, donor)
      const [c, o] = this._residueGetCO(acceptor)

      if (n === null || h === null || c === null || o === null) {
        return result
      }

      const distanceHO = h.distanceTo(o)
      const distanceHC = h.distanceTo(c)
      const distanceNC = n.distanceTo(c)
      const distanceNO = n.distanceTo(o)

      if (
        distanceHO < MINIMAL_DISTANCE ||
        distanceHC < MINIMAL_DISTANCE ||
        distanceNC < MINIMAL_DISTANCE ||
        distanceNO < MINIMAL_DISTANCE
      ) {
        result = MIN_HBOND_ENERGY
      } else {
        result =
          COUPLING_CONSTANT / distanceHO -
          COUPLING_CONSTANT / distanceHC +
          COUPLING_CONSTANT / distanceNC -
          COUPLING_CONSTANT / distanceNO
      }

      // DSSP compatibility mode:
      result = Math.round(result * 1000) / 1000

      if (result < MIN_HBOND_ENERGY) {
        result = MIN_HBOND_ENERGY
      }
    }

    // update donor
    if (typeof this._hbonds[donor._index] === 'undefined') {
      this._hbonds[donor._index] = {
        donor: [],
        acceptor: []
      }
    }
    const donorInfo = this._hbonds[donor._index]

    if (donorInfo.acceptor.length < 2) {
      donorInfo.acceptor.push({
        residue: acceptor._index,
        energy: result
      })
    }

    if (donorInfo.acceptor.length > 1) {
      if (result < donorInfo.acceptor[0].energy) {
        donorInfo.acceptor[1].residue = donorInfo.acceptor[0].residue
        donorInfo.acceptor[1].energy = donorInfo.acceptor[0].energy
        donorInfo.acceptor[0].residue = acceptor._index
        donorInfo.acceptor[0].energy = result
      } else if (result < donorInfo.acceptor[1].energy) {
        donorInfo.acceptor[1].residue = acceptor._index
        donorInfo.acceptor[1].energy = result
      }
    }

    // update acceptor
    if (typeof this._hbonds[acceptor._index] === 'undefined') {
      this._hbonds[acceptor._index] = {
        donor: [],
        acceptor: []
      }
    }
    const accInfo = this._hbonds[acceptor._index]

    if (accInfo.donor.length < 2) {
      accInfo.donor.push({
        residue: donor._index,
        energy: result
      })
    }

    if (accInfo.donor.length > 1) {
      if (result < accInfo.donor[0].energy) {
        accInfo.donor[1].residue = accInfo.donor[0].residue
        accInfo.donor[1].energy = accInfo.donor[0].energy
        accInfo.donor[0].residue = donor._index
        accInfo.donor[0].energy = result
      } else if (result < accInfo.donor[1].energy) {
        accInfo.donor[1].residue = donor._index
        accInfo.donor[1].energy = result
      }
    }

    return result
  }
}