hydrogen-bonds.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. /**
  2. * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  5. * @author Fred Ludlow <Fred.Ludlow@astx.com>
  6. *
  7. * based in part on NGL (https://github.com/arose/ngl)
  8. */
  9. import { ParamDefinition as PD } from '../../../mol-util/param-definition';
  10. import { Structure, Unit, StructureElement } from '../../../mol-model/structure';
  11. import { AtomGeometry, AtomGeometryAngles, calcAngles, calcPlaneAngle } from '../chemistry/geometry';
  12. import { FeaturesBuilder, Features } from './features';
  13. import { typeSymbol, bondToElementCount, bondCount, formalCharge, compId, atomId } from '../chemistry/util';
  14. import { Elements } from '../../../mol-model/structure/model/properties/atomic/types';
  15. import { ValenceModelProvider } from '../valence-model';
  16. import { degToRad } from '../../../mol-math/misc';
  17. import { FeatureType, FeatureGroup, InteractionType } from './common';
  18. import { ContactProvider } from './contacts';
  19. import { MoleculeType, ProteinBackboneAtoms } from '../../../mol-model/structure/model/types';
  20. const GeometryParams = {
  21. distanceMax: PD.Numeric(3.5, { min: 1, max: 5, step: 0.1 }),
  22. backbone: PD.Boolean(false, { description: 'Include backbone-to-backbone hydrogen bonds' }),
  23. accAngleDevMax: PD.Numeric(45, { min: 0, max: 180, step: 1 }, { description: 'Max deviation from ideal acceptor angle' }),
  24. donAngleDevMax: PD.Numeric(45, { min: 0, max: 180, step: 1 }, { description: 'Max deviation from ideal donor angle' }),
  25. accOutOfPlaneAngleMax: PD.Numeric(90, { min: 0, max: 180, step: 1 }),
  26. donOutOfPlaneAngleMax: PD.Numeric(45, { min: 0, max: 180, step: 1 }),
  27. }
  28. type GeometryParams = typeof GeometryParams
  29. type GeometryProps = PD.Values<GeometryParams>
  30. const HydrogenBondsParams = {
  31. ...GeometryParams,
  32. water: PD.Boolean(false, { description: 'Include water-to-water hydrogen bonds' }),
  33. sulfurDistanceMax: PD.Numeric(4.1, { min: 1, max: 5, step: 0.1 }),
  34. }
  35. type HydrogenBondsParams = typeof HydrogenBondsParams
  36. type HydrogenBondsProps = PD.Values<HydrogenBondsParams>
  37. const WeakHydrogenBondsParams = {
  38. ...GeometryParams,
  39. }
  40. type WeakHydrogenBondsParams = typeof WeakHydrogenBondsParams
  41. type WeakHydrogenBondsProps = PD.Values<WeakHydrogenBondsParams>
  42. //
  43. // Geometric characteristics of hydrogen bonds involving sulfur atoms in proteins
  44. // https://doi.org/10.1002/prot.22327
  45. // Satisfying Hydrogen Bonding Potential in Proteins (HBPLUS)
  46. // https://doi.org/10.1006/jmbi.1994.1334
  47. // http://www.csb.yale.edu/userguides/datamanip/hbplus/hbplus_descrip.html
  48. function getUnitValenceModel(structure: Structure, unit: Unit.Atomic) {
  49. const valenceModel = ValenceModelProvider.getValue(structure).value
  50. if (!valenceModel) throw Error('expected valence model to be available')
  51. const unitValenceModel = valenceModel.get(unit.id)
  52. if (!unitValenceModel) throw Error('expected valence model for unit to be available')
  53. return unitValenceModel
  54. }
  55. /**
  56. * Potential hydrogen donor
  57. */
  58. function addUnitHydrogenDonors(structure: Structure, unit: Unit.Atomic, builder: FeaturesBuilder) {
  59. const { totalH } = getUnitValenceModel(structure, unit)
  60. const { elements } = unit
  61. const { x, y, z } = unit.model.atomicConformation
  62. for (let i = 0 as StructureElement.UnitIndex, il = elements.length; i < il; ++i) {
  63. const element = typeSymbol(unit, i)
  64. if ((
  65. // include both nitrogen atoms in histidine due to
  66. // their often ambiguous protonation assignment
  67. isHistidineNitrogen(unit, i)
  68. ) || (
  69. totalH[i] > 0 &&
  70. (element === Elements.N || element === Elements.O || element === Elements.S)
  71. )
  72. ) {
  73. builder.add(FeatureType.HydrogenDonor, FeatureGroup.None, x[elements[i]], y[elements[i]], z[elements[i]], i)
  74. }
  75. }
  76. }
  77. /**
  78. * Weak hydrogen donor.
  79. */
  80. function addUnitWeakHydrogenDonors(structure: Structure, unit: Unit.Atomic, builder: FeaturesBuilder) {
  81. const { totalH } = getUnitValenceModel(structure, unit)
  82. const { elements } = unit
  83. const { x, y, z } = unit.model.atomicConformation
  84. for (let i = 0 as StructureElement.UnitIndex, il = elements.length; i < il; ++i) {
  85. if (
  86. typeSymbol(unit, i) === Elements.C &&
  87. totalH[i] > 0 &&
  88. (
  89. bondToElementCount(structure, unit, i, Elements.N) > 0 ||
  90. bondToElementCount(structure, unit, i, Elements.O) > 0 ||
  91. inAromaticRingWithElectronNegativeElement(structure, unit, i)
  92. )
  93. ) {
  94. builder.add(FeatureType.WeakHydrogenDonor, FeatureGroup.None, x[elements[i]], y[elements[i]], z[elements[i]], i)
  95. }
  96. }
  97. }
  98. function inAromaticRingWithElectronNegativeElement(structure: Structure, unit: Unit.Atomic, index: StructureElement.UnitIndex) {
  99. return false // TODO
  100. // if (!a.isAromatic()) return false
  101. // const ringData = a.residueType.getRings()
  102. // if (!ringData) return false
  103. // let hasElement = false
  104. // const rings = ringData.rings
  105. // rings.forEach(ring => {
  106. // if (hasElement) return // already found one
  107. // if (ring.some(idx => (a.index - a.residueAtomOffset) === idx)) { // in ring
  108. // hasElement = ring.some(idx => {
  109. // const atomTypeId = a.residueType.atomTypeIdList[ idx ]
  110. // const number = a.atomMap.get(atomTypeId).number
  111. // return number === Elements.N || number === Elements.O
  112. // })
  113. // }
  114. // })
  115. // return hasElement
  116. }
  117. /**
  118. * Potential hydrogen acceptor
  119. */
  120. function addUnitHydrogenAcceptors(structure: Structure, unit: Unit.Atomic, builder: FeaturesBuilder) {
  121. const { charge, implicitH, idealGeometry } = getUnitValenceModel(structure, unit)
  122. const { elements } = unit
  123. const { x, y, z } = unit.model.atomicConformation
  124. const add = (i: StructureElement.UnitIndex) => {
  125. builder.add(FeatureType.HydrogenAcceptor, FeatureGroup.None, x[elements[i]], y[elements[i]], z[elements[i]], i)
  126. }
  127. for (let i = 0 as StructureElement.UnitIndex, il = elements.length; i < il; ++i) {
  128. const element = typeSymbol(unit, i)
  129. if (element === Elements.O) {
  130. // Basically assume all oxygen atoms are acceptors!
  131. add(i)
  132. } else if (element === Elements.N) {
  133. if (isHistidineNitrogen(unit, i)) {
  134. // include both nitrogen atoms in histidine due to
  135. // their often ambiguous protonation assignment
  136. add(i)
  137. } else if (charge[i] < 1) {
  138. // Neutral nitrogen might be an acceptor
  139. // It must have at least one lone pair not conjugated
  140. const totalBonds = bondCount(structure, unit, i) + implicitH[i]
  141. const ig = idealGeometry[i]
  142. if (
  143. (ig === AtomGeometry.Tetrahedral && totalBonds < 4) ||
  144. (ig === AtomGeometry.Trigonal && totalBonds < 3) ||
  145. (ig === AtomGeometry.Linear && totalBonds < 2)
  146. ) {
  147. add(i)
  148. }
  149. }
  150. } else if (element === Elements.S) {
  151. const resname = compId(unit, i)
  152. if (resname === 'CYS' || resname === 'MET' || formalCharge(unit, i) === -1) {
  153. add(i)
  154. }
  155. }
  156. }
  157. }
  158. function isWater(unit: Unit.Atomic, index: StructureElement.UnitIndex) {
  159. return unit.model.atomicHierarchy.derived.residue.moleculeType[unit.elements[index]] === MoleculeType.Water
  160. }
  161. function isBackbone(unit: Unit.Atomic, index: StructureElement.UnitIndex) {
  162. return ProteinBackboneAtoms.has(atomId(unit, index))
  163. }
  164. function isRing(unit: Unit.Atomic, index: StructureElement.UnitIndex) {
  165. return unit.rings.elementRingIndices.has(index)
  166. }
  167. function isHistidineNitrogen(unit: Unit.Atomic, index: StructureElement.UnitIndex) {
  168. return compId(unit, index) === 'HIS' && typeSymbol(unit, index) === Elements.N && isRing(unit, index)
  169. }
  170. function isBackboneHydrogenBond(unitA: Unit.Atomic, indexA: StructureElement.UnitIndex, unitB: Unit.Atomic, indexB: StructureElement.UnitIndex) {
  171. return isBackbone(unitA, indexA) && isBackbone(unitB, indexB)
  172. }
  173. function isWaterHydrogenBond(unitA: Unit.Atomic, indexA: StructureElement.UnitIndex, unitB: Unit.Atomic, indexB: StructureElement.UnitIndex) {
  174. return isWater(unitA, indexA) && isWater(unitB, indexB)
  175. }
  176. function isHydrogenBond(ti: FeatureType, tj: FeatureType) {
  177. return (
  178. (ti === FeatureType.HydrogenAcceptor && tj === FeatureType.HydrogenDonor) ||
  179. (ti === FeatureType.HydrogenDonor && tj === FeatureType.HydrogenAcceptor)
  180. )
  181. }
  182. function isWeakHydrogenBond(ti: FeatureType, tj: FeatureType) {
  183. return (
  184. (ti === FeatureType.WeakHydrogenDonor && tj === FeatureType.HydrogenAcceptor) ||
  185. (ti === FeatureType.HydrogenAcceptor && tj === FeatureType.WeakHydrogenDonor)
  186. )
  187. }
  188. function getGeometryOptions(props: GeometryProps) {
  189. return {
  190. includeBackbone: props.backbone,
  191. maxAccAngleDev: degToRad(props.accAngleDevMax),
  192. maxDonAngleDev: degToRad(props.donAngleDevMax),
  193. maxAccOutOfPlaneAngle: degToRad(props.accOutOfPlaneAngleMax),
  194. maxDonOutOfPlaneAngle: degToRad(props.donOutOfPlaneAngleMax),
  195. }
  196. }
  197. type GeometryOptions = ReturnType<typeof getGeometryOptions>
  198. function getHydrogenBondsOptions(props: HydrogenBondsProps) {
  199. return {
  200. ...getGeometryOptions(props),
  201. includeWater: props.water,
  202. maxSulfurDistSq: props.sulfurDistanceMax * props.sulfurDistanceMax,
  203. maxDistSq: props.distanceMax * props.distanceMax
  204. }
  205. }
  206. type HydrogenBondsOptions = ReturnType<typeof getHydrogenBondsOptions>
  207. const deg120InRad = degToRad(120)
  208. function checkGeometry(structure: Structure, don: Features.Info, acc: Features.Info, opts: GeometryOptions): true | undefined {
  209. const donIndex = don.members[don.offsets[don.feature]]
  210. const accIndex = acc.members[acc.offsets[acc.feature]]
  211. if (!opts.includeBackbone && isBackboneHydrogenBond(don.unit, donIndex, acc.unit, accIndex)) return
  212. const donAngles = calcAngles(structure, don.unit, donIndex, acc.unit, accIndex)
  213. const idealDonAngle = AtomGeometryAngles.get(don.idealGeometry[donIndex]) || deg120InRad
  214. if (donAngles.some(donAngle => Math.abs(idealDonAngle - donAngle) > opts.maxDonAngleDev)) return
  215. if (don.idealGeometry[donIndex] === AtomGeometry.Trigonal) {
  216. const outOfPlane = calcPlaneAngle(structure, don.unit, donIndex, acc.unit, accIndex)
  217. if (outOfPlane !== undefined && outOfPlane > opts.maxDonOutOfPlaneAngle) return
  218. }
  219. const accAngles = calcAngles(structure, acc.unit, accIndex, don.unit, donIndex)
  220. const idealAccAngle = AtomGeometryAngles.get(acc.idealGeometry[accIndex]) || deg120InRad
  221. // Do not limit large acceptor angles
  222. if (accAngles.some(accAngle => idealAccAngle - accAngle > opts.maxAccAngleDev)) return
  223. if (acc.idealGeometry[accIndex] === AtomGeometry.Trigonal) {
  224. const outOfPlane = calcPlaneAngle(structure, acc.unit, accIndex, don.unit, donIndex)
  225. if (outOfPlane !== undefined && outOfPlane > opts.maxAccOutOfPlaneAngle) return
  226. }
  227. return true
  228. }
  229. function testHydrogenBond(structure: Structure, infoA: Features.Info, infoB: Features.Info, distanceSq: number, opts: HydrogenBondsOptions): InteractionType | undefined {
  230. const typeA = infoA.types[infoA.feature]
  231. const typeB = infoB.types[infoB.feature]
  232. if (!isHydrogenBond(typeA, typeB)) return
  233. const [don, acc] = typeB === FeatureType.HydrogenAcceptor ? [infoA, infoB] : [infoB, infoA]
  234. const donIndex = don.members[don.offsets[don.feature]]
  235. const accIndex = acc.members[acc.offsets[acc.feature]]
  236. // check if distance is ok depending on non-sulfur-containing hbond
  237. const maxDistSq = typeSymbol(don.unit, donIndex) === Elements.S || typeSymbol(acc.unit, accIndex) === Elements.S ? opts.maxSulfurDistSq : opts.maxDistSq
  238. if (distanceSq > maxDistSq) return
  239. if (!opts.includeWater && isWaterHydrogenBond(don.unit, donIndex, acc.unit, accIndex)) return
  240. if (!checkGeometry(structure, don, acc, opts)) return
  241. return InteractionType.HydrogenBond
  242. }
  243. function testWeakHydrogenBond(structure: Structure, infoA: Features.Info, infoB: Features.Info, distanceSq: number, opts: GeometryOptions): InteractionType | undefined {
  244. const typeA = infoA.types[infoA.feature]
  245. const typeB = infoB.types[infoB.feature]
  246. if (!isWeakHydrogenBond(typeA, typeB)) return
  247. const [don, acc] = typeB === FeatureType.HydrogenAcceptor ? [infoA, infoB] : [infoB, infoA]
  248. if (!checkGeometry(structure, don, acc, opts)) return
  249. return InteractionType.WeakHydrogenBond
  250. }
  251. //
  252. export const HydrogenDonorProvider = Features.Provider([FeatureType.HydrogenDonor], addUnitHydrogenDonors)
  253. export const WeakHydrogenDonorProvider = Features.Provider([FeatureType.WeakHydrogenDonor], addUnitWeakHydrogenDonors)
  254. export const HydrogenAcceptorProvider = Features.Provider([FeatureType.HydrogenAcceptor], addUnitHydrogenAcceptors)
  255. export const HydrogenBondsProvider: ContactProvider<HydrogenBondsParams> = {
  256. name: 'hydrogen-bonds',
  257. params: HydrogenBondsParams,
  258. createTester: (props: HydrogenBondsProps) => {
  259. const maxDistance = Math.max(props.distanceMax, props.sulfurDistanceMax)
  260. const opts = getHydrogenBondsOptions(props)
  261. return {
  262. maxDistance,
  263. requiredFeatures: new Set([FeatureType.HydrogenDonor, FeatureType.HydrogenAcceptor]),
  264. getType: (structure, infoA, infoB, distanceSq) => testHydrogenBond(structure, infoA, infoB, distanceSq, opts)
  265. }
  266. }
  267. }
  268. export const WeakHydrogenBondsProvider: ContactProvider<WeakHydrogenBondsParams> = {
  269. name: 'weak-hydrogen-bonds',
  270. params: WeakHydrogenBondsParams,
  271. createTester: (props: WeakHydrogenBondsProps) => {
  272. const opts = getGeometryOptions(props)
  273. return {
  274. maxDistance: props.distanceMax,
  275. requiredFeatures: new Set([FeatureType.WeakHydrogenDonor, FeatureType.HydrogenAcceptor]),
  276. getType: (structure, infoA, infoB, distanceSq) => testWeakHydrogenBond(structure, infoA, infoB, distanceSq, opts)
  277. }
  278. }
  279. }