hydrogen-bonds.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  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. */
  6. import { ParamDefinition as PD } from '../../../mol-util/param-definition';
  7. import { Structure, Unit, StructureElement } from '../../../mol-model/structure';
  8. import { AtomGeometry, AtomGeometryAngles, calcAngles, calcPlaneAngle } from '../chemistry/geometry';
  9. import { FeaturesBuilder, Features } from './features';
  10. import { MoleculeType, ProteinBackboneAtoms } from '../../../mol-model/structure/model/types';
  11. import { typeSymbol, bondToElementCount, bondCount, formalCharge, atomId, compId, altLoc, connectedTo } from '../chemistry/util';
  12. import { Elements } from '../../../mol-model/structure/model/properties/atomic/types';
  13. import { ValenceModelProvider } from '../valence-model';
  14. import { degToRad } from '../../../mol-math/misc';
  15. import { FeatureType, FeatureGroup, InteractionType } from './common';
  16. import { IntraLinksBuilder, InterLinksBuilder } from './builder';
  17. import { Mat4, Vec3 } from '../../../mol-math/linear-algebra';
  18. export interface HydrogenBonds {
  19. }
  20. export const HydrogenBondsParams = {
  21. maxDist: PD.Numeric(3.5, { min: 1, max: 5, step: 0.1 }),
  22. maxSulfurDist: PD.Numeric(4.1, { min: 1, max: 5, step: 0.1 }),
  23. maxAccAngleDev: PD.Numeric(45, { min: 0, max: 180, step: 1 }, { description: 'Max deviation from ideal acceptor angle' }),
  24. maxDonAngleDev: PD.Numeric(45, { min: 0, max: 180, step: 1 }, { description: 'Max deviation from ideal donor angle' }),
  25. maxAccOutOfPlaneAngle: PD.Numeric(90, { min: 0, max: 180, step: 1 }),
  26. maxDonOutOfPlaneAngle: PD.Numeric(45, { min: 0, max: 180, step: 1 }),
  27. }
  28. export type HydrogenBondsParams = typeof HydrogenBondsParams
  29. export type HydrogenBondsProps = PD.Values<HydrogenBondsParams>
  30. //
  31. // Geometric characteristics of hydrogen bonds involving sulfur atoms in proteins
  32. // https://doi.org/10.1002/prot.22327
  33. // Satisfying Hydrogen Bonding Potential in Proteins (HBPLUS)
  34. // https://doi.org/10.1006/jmbi.1994.1334
  35. // http://www.csb.yale.edu/userguides/datamanip/hbplus/hbplus_descrip.html
  36. function getUnitValenceModel(structure: Structure, unit: Unit.Atomic) {
  37. const valenceModel = ValenceModelProvider.getValue(structure).value
  38. if (!valenceModel) throw Error('expected valence model to be available')
  39. const unitValenceModel = valenceModel.get(unit.id)
  40. if (!unitValenceModel) throw Error('expected valence model for unit to be available')
  41. return unitValenceModel
  42. }
  43. /**
  44. * Potential hydrogen donor
  45. */
  46. export function addUnitHydrogenDonors(structure: Structure, unit: Unit.Atomic, builder: FeaturesBuilder) {
  47. const { totalH } = getUnitValenceModel(structure, unit)
  48. const { elements } = unit
  49. const { x, y, z } = unit.model.atomicConformation
  50. for (let i = 0 as StructureElement.UnitIndex, il = elements.length; i < il; ++i) {
  51. const element = typeSymbol(unit, i)
  52. if ((
  53. // include both nitrogen atoms in histidine due to
  54. // their often ambiguous protonation assignment
  55. isHistidineNitrogen(unit, i)
  56. ) || (
  57. totalH[i] > 0 &&
  58. (element === Elements.N || element === Elements.O || element === Elements.S)
  59. )
  60. ) {
  61. builder.addOne(FeatureType.HydrogenDonor, FeatureGroup.None, x[elements[i]], y[elements[i]], z[elements[i]], i)
  62. }
  63. }
  64. }
  65. /**
  66. * Weak hydrogen donor.
  67. */
  68. export function addUnitWeakHydrogenDonors(structure: Structure, unit: Unit.Atomic, builder: FeaturesBuilder) {
  69. const { totalH } = getUnitValenceModel(structure, unit)
  70. const { elements } = unit
  71. const { x, y, z } = unit.model.atomicConformation
  72. for (let i = 0 as StructureElement.UnitIndex, il = elements.length; i < il; ++i) {
  73. if (
  74. typeSymbol(unit, i) === Elements.C &&
  75. totalH[i] > 0 &&
  76. (
  77. bondToElementCount(structure, unit, i, Elements.N) > 0 ||
  78. bondToElementCount(structure, unit, i, Elements.O) > 0 ||
  79. inAromaticRingWithElectronNegativeElement(structure, unit, i)
  80. )
  81. ) {
  82. builder.addOne(FeatureType.WeakHydrogenDonor, FeatureGroup.None, x[elements[i]], y[elements[i]], z[elements[i]], i)
  83. }
  84. }
  85. }
  86. function inAromaticRingWithElectronNegativeElement(structure: Structure, unit: Unit.Atomic, index: StructureElement.UnitIndex) {
  87. return false // TODO
  88. // if (!a.isAromatic()) return false
  89. // const ringData = a.residueType.getRings()
  90. // if (!ringData) return false
  91. // let hasElement = false
  92. // const rings = ringData.rings
  93. // rings.forEach(ring => {
  94. // if (hasElement) return // already found one
  95. // if (ring.some(idx => (a.index - a.residueAtomOffset) === idx)) { // in ring
  96. // hasElement = ring.some(idx => {
  97. // const atomTypeId = a.residueType.atomTypeIdList[ idx ]
  98. // const number = a.atomMap.get(atomTypeId).number
  99. // return number === Elements.N || number === Elements.O
  100. // })
  101. // }
  102. // })
  103. // return hasElement
  104. }
  105. /**
  106. * Potential hydrogen acceptor
  107. */
  108. export function addUnitHydrogenAcceptors(structure: Structure, unit: Unit.Atomic, builder: FeaturesBuilder) {
  109. const { charge, implicitH, idealGeometry } = getUnitValenceModel(structure, unit)
  110. const { elements } = unit
  111. const { x, y, z } = unit.model.atomicConformation
  112. function add(i: StructureElement.UnitIndex) {
  113. builder.addOne(FeatureType.HydrogenAcceptor, FeatureGroup.None, x[elements[i]], y[elements[i]], z[elements[i]], i)
  114. }
  115. for (let i = 0 as StructureElement.UnitIndex, il = elements.length; i < il; ++i) {
  116. const element = typeSymbol(unit, i)
  117. if (element === Elements.O) {
  118. // Basically assume all oxygen atoms are acceptors!
  119. add(i)
  120. } else if (element === Elements.N) {
  121. if (isHistidineNitrogen(unit, i)) {
  122. // include both nitrogen atoms in histidine due to
  123. // their often ambiguous protonation assignment
  124. add(i)
  125. } else if (charge[i] < 1) {
  126. // Neutral nitrogen might be an acceptor
  127. // It must have at least one lone pair not conjugated
  128. const totalBonds = bondCount(structure, unit, i) + implicitH[i]
  129. const ig = idealGeometry[i]
  130. if (
  131. (ig === AtomGeometry.Tetrahedral && totalBonds < 4) ||
  132. (ig === AtomGeometry.Trigonal && totalBonds < 3) ||
  133. (ig === AtomGeometry.Linear && totalBonds < 2)
  134. ) {
  135. add(i)
  136. }
  137. }
  138. } else if (element === Elements.S) {
  139. const resname = compId(unit, i)
  140. if (resname === 'CYS' || resname === 'MET' || formalCharge(unit, i) === -1) {
  141. add(i)
  142. }
  143. }
  144. }
  145. }
  146. function isWater(unit: Unit.Atomic, index: StructureElement.UnitIndex) {
  147. return unit.model.atomicHierarchy.derived.residue.moleculeType[unit.elements[index]] === MoleculeType.Water
  148. }
  149. function isBackbone(unit: Unit.Atomic, index: StructureElement.UnitIndex) {
  150. return ProteinBackboneAtoms.has(atomId(unit, index))
  151. }
  152. function isRing(unit: Unit.Atomic, index: StructureElement.UnitIndex) {
  153. return unit.rings.elementRingIndices.has(index)
  154. }
  155. function isHistidineNitrogen(unit: Unit.Atomic, index: StructureElement.UnitIndex) {
  156. return compId(unit, index) === 'HIS' && typeSymbol(unit, index) === Elements.N && isRing(unit, index)
  157. }
  158. function isBackboneHydrogenBond(unitA: Unit.Atomic, indexA: StructureElement.UnitIndex, unitB: Unit.Atomic, indexB: StructureElement.UnitIndex) {
  159. return isBackbone(unitA, indexA) && isBackbone(unitB, indexB)
  160. }
  161. function isWaterHydrogenBond(unitA: Unit.Atomic, indexA: StructureElement.UnitIndex, unitB: Unit.Atomic, indexB: StructureElement.UnitIndex) {
  162. return isWater(unitA, indexA) && isWater(unitB, indexB)
  163. }
  164. function isHydrogenBond(ti: FeatureType, tj: FeatureType) {
  165. return (
  166. (ti === FeatureType.HydrogenAcceptor && tj === FeatureType.HydrogenDonor) ||
  167. (ti === FeatureType.HydrogenDonor && tj === FeatureType.HydrogenAcceptor)
  168. )
  169. }
  170. function isWeakHydrogenBond(ti: FeatureType, tj: FeatureType) {
  171. return (
  172. (ti === FeatureType.WeakHydrogenDonor && tj === FeatureType.HydrogenAcceptor) ||
  173. (ti === FeatureType.HydrogenAcceptor && tj === FeatureType.WeakHydrogenDonor)
  174. )
  175. }
  176. function getHydrogenBondType(unitA: Unit.Atomic, indexA: StructureElement.UnitIndex, unitB: Unit.Atomic, indexB: StructureElement.UnitIndex) {
  177. if (isWaterHydrogenBond(unitA, indexA, unitB, indexB)) {
  178. return InteractionType.WaterHydrogenBond
  179. } else if (isBackboneHydrogenBond(unitA, indexA, unitB, indexB)) {
  180. return InteractionType.BackboneHydrogenBond
  181. } else {
  182. return InteractionType.HydrogenBond
  183. }
  184. }
  185. interface Info {
  186. unit: Unit.Atomic,
  187. types: ArrayLike<FeatureType>,
  188. feature: number,
  189. members: ArrayLike<StructureElement.UnitIndex>,
  190. offsets: ArrayLike<number>,
  191. idealGeometry: Int8Array
  192. }
  193. function Info(structure: Structure, unit: Unit.Atomic, features: Features) {
  194. const valenceModel = ValenceModelProvider.getValue(structure).value
  195. if (!valenceModel || !valenceModel.has(unit.id)) throw new Error('valence model required')
  196. return {
  197. unit,
  198. types: features.types,
  199. members: features.members,
  200. offsets: features.offsets,
  201. idealGeometry: valenceModel.get(unit.id)!.idealGeometry
  202. } as Info
  203. }
  204. function getOptions(props: HydrogenBondsProps) {
  205. return {
  206. maxAccAngleDev: degToRad(props.maxAccAngleDev),
  207. maxDonAngleDev: degToRad(props.maxDonAngleDev),
  208. maxAccOutOfPlaneAngle: degToRad(props.maxAccOutOfPlaneAngle),
  209. maxDonOutOfPlaneAngle: degToRad(props.maxDonOutOfPlaneAngle),
  210. maxDist: Math.max(props.maxDist, props.maxSulfurDist),
  211. maxHbondDistSq: props.maxDist * props.maxDist,
  212. }
  213. }
  214. type Options = ReturnType<typeof getOptions>
  215. function testHydrogenBond(dSq: number, structure: Structure, infoA: Info, infoB: Info, opts: Options): InteractionType | undefined {
  216. const typeA = infoA.types[infoA.feature]
  217. const typeB = infoB.types[infoB.feature]
  218. const isWeak = isWeakHydrogenBond(typeA, typeB)
  219. if (!isWeak && !isHydrogenBond(typeA, typeB)) return
  220. const [don, acc] = typeA === FeatureType.HydrogenAcceptor ? [infoA, infoB] : [infoB, infoA]
  221. const donIndex = don.members[don.offsets[don.feature]]
  222. const accIndex = acc.members[acc.offsets[acc.feature]]
  223. if (accIndex === donIndex) return // DA to self
  224. const altD = altLoc(don.unit, donIndex)
  225. const altA = altLoc(acc.unit, accIndex)
  226. if (altD && altA && altD !== altA) return // incompatible alternate location id
  227. if (don.unit.residueIndex[donIndex] === acc.unit.residueIndex[accIndex]) return // same residue
  228. // check if distance is ok for non-sulfur-containing hbond
  229. if (typeSymbol(don.unit, donIndex) !== Elements.S && typeSymbol(acc.unit, accIndex) !== Elements.S && dSq > opts.maxHbondDistSq) return
  230. // no hbond if donor and acceptor are bonded
  231. if (connectedTo(structure, don.unit, donIndex, acc.unit, accIndex)) return
  232. const donAngles = calcAngles(structure, don.unit, donIndex, acc.unit, accIndex)
  233. const idealDonAngle = AtomGeometryAngles.get(don.idealGeometry[donIndex]) || degToRad(120)
  234. if (donAngles.some(donAngle => Math.abs(idealDonAngle - donAngle) > opts.maxDonAngleDev)) return
  235. if (don.idealGeometry[donIndex] === AtomGeometry.Trigonal) {
  236. const outOfPlane = calcPlaneAngle(structure, don.unit, donIndex, acc.unit, accIndex)
  237. if (outOfPlane !== undefined && outOfPlane > opts.maxDonOutOfPlaneAngle) return
  238. }
  239. const accAngles = calcAngles(structure, acc.unit, accIndex, don.unit, donIndex)
  240. const idealAccAngle = AtomGeometryAngles.get(acc.idealGeometry[accIndex]) || degToRad(120)
  241. // Do not limit large acceptor angles
  242. if (accAngles.some(accAngle => idealAccAngle - accAngle > opts.maxAccAngleDev)) return
  243. if (acc.idealGeometry[accIndex] === AtomGeometry.Trigonal) {
  244. const outOfPlane = calcPlaneAngle(structure, acc.unit, accIndex, don.unit, donIndex)
  245. if (outOfPlane !== undefined && outOfPlane > opts.maxAccOutOfPlaneAngle) return
  246. }
  247. return isWeak ? InteractionType.WeakHydrogenBond : getHydrogenBondType(don.unit, donIndex, acc.unit, accIndex)
  248. }
  249. /**
  250. * All intra-unit pairs of hydrogen donor and acceptor atoms
  251. */
  252. export function addUnitHydrogenBonds(structure: Structure, unit: Unit.Atomic, features: Features, builder: IntraLinksBuilder, props: HydrogenBondsProps) {
  253. const opts = getOptions(props)
  254. const { x, y, z, count, lookup3d } = features
  255. const infoA = Info(structure, unit, features)
  256. const infoB = { ...infoA }
  257. for (let i = 0; i < count; ++i) {
  258. const { count, indices, squaredDistances } = lookup3d.find(x[i], y[i], z[i], opts.maxDist)
  259. if (count === 0) continue
  260. infoA.feature = i
  261. for (let r = 0; r < count; ++r) {
  262. const j = indices[r]
  263. if (j <= i) continue
  264. infoB.feature = j
  265. const bondType = testHydrogenBond(squaredDistances[r], structure, infoA, infoB, opts)
  266. if (bondType) builder.add(i, j, bondType)
  267. }
  268. }
  269. }
  270. //
  271. const _imageTransform = Mat4()
  272. /**
  273. * All inter-unit pairs of hydrogen donor and acceptor atoms
  274. */
  275. export function addStructureHydrogenBonds(structure: Structure, unitA: Unit.Atomic, featuresA: Features, unitB: Unit.Atomic, featuresB: Features, builder: InterLinksBuilder, props: HydrogenBondsProps) {
  276. const opts = getOptions(props)
  277. const { count, x: xA, y: yA, z: zA } = featuresA;
  278. const { lookup3d } = featuresB;
  279. // the lookup queries need to happen in the "unitB space".
  280. // that means imageA = inverseOperB(operA(i))
  281. const imageTransform = Mat4.mul(_imageTransform, unitB.conformation.operator.inverse, unitA.conformation.operator.matrix)
  282. const isNotIdentity = !Mat4.isIdentity(imageTransform)
  283. const imageA = Vec3()
  284. const { center: bCenter, radius: bRadius } = lookup3d.boundary.sphere;
  285. const testDistanceSq = (bRadius + opts.maxDist) * (bRadius + opts.maxDist);
  286. const infoA = Info(structure, unitA, featuresA)
  287. const infoB = Info(structure, unitB, featuresB)
  288. builder.startUnitPair(unitA, unitB)
  289. for (let i = 0; i < count; ++i) {
  290. Vec3.set(imageA, xA[i], yA[i], zA[i])
  291. if (isNotIdentity) Vec3.transformMat4(imageA, imageA, imageTransform)
  292. if (Vec3.squaredDistance(imageA, bCenter) > testDistanceSq) continue
  293. const { indices, count, squaredDistances } = lookup3d.find(imageA[0], imageA[1], imageA[2], opts.maxDist)
  294. if (count === 0) continue
  295. infoA.feature = i
  296. for (let r = 0; r < count; ++r) {
  297. const j = indices[r]
  298. if (j <= i) continue
  299. infoB.feature = j
  300. const bondType = testHydrogenBond(squaredDistances[r], structure, infoA, infoB, opts)
  301. if (bondType) builder.add(i, j, bondType)
  302. }
  303. }
  304. builder.finishUnitPair()
  305. }