hydrogen-bonds.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. /**
  2. * Copyright (c) 2019-2020 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(true, { 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.get(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. const { elementAromaticRingIndices } = unit.rings;
  63. for (let i = 0 as StructureElement.UnitIndex, il = elements.length; i < il; ++i) {
  64. if (elementAromaticRingIndices.has(i)) continue;
  65. const element = typeSymbol(unit, i);
  66. if ((
  67. // include both nitrogen atoms in histidine due to
  68. // their often ambiguous protonation assignment
  69. isHistidineNitrogen(unit, i)
  70. ) || (
  71. totalH[i] > 0 &&
  72. (element === Elements.N || element === Elements.O || element === Elements.S)
  73. )) {
  74. builder.add(FeatureType.HydrogenDonor, FeatureGroup.None, x[elements[i]], y[elements[i]], z[elements[i]], i);
  75. }
  76. }
  77. }
  78. /**
  79. * Weak hydrogen donor.
  80. */
  81. function addUnitWeakHydrogenDonors(structure: Structure, unit: Unit.Atomic, builder: FeaturesBuilder) {
  82. const { totalH } = getUnitValenceModel(structure, unit);
  83. const { elements } = unit;
  84. const { x, y, z } = unit.model.atomicConformation;
  85. for (let i = 0 as StructureElement.UnitIndex, il = elements.length; i < il; ++i) {
  86. if (
  87. typeSymbol(unit, i) === Elements.C &&
  88. totalH[i] > 0 &&
  89. (
  90. bondToElementCount(structure, unit, i, Elements.N) > 0 ||
  91. bondToElementCount(structure, unit, i, Elements.O) > 0 ||
  92. inAromaticRingWithElectronNegativeElement(unit, i)
  93. )
  94. ) {
  95. builder.add(FeatureType.WeakHydrogenDonor, FeatureGroup.None, x[elements[i]], y[elements[i]], z[elements[i]], i);
  96. }
  97. }
  98. }
  99. function inAromaticRingWithElectronNegativeElement(unit: Unit.Atomic, index: StructureElement.UnitIndex) {
  100. const { elementAromaticRingIndices, all } = unit.rings;
  101. const ringIndices = elementAromaticRingIndices.get(index);
  102. if (ringIndices === undefined) return false;
  103. for (let i = 0, il = ringIndices.length; i < il; ++i) {
  104. const ring = all[ringIndices[i]];
  105. for (let j = 0, jl = ring.length; j < jl; ++j) {
  106. const element = typeSymbol(unit, ring[j]);
  107. if (element === Elements.N || element === Elements.O) {
  108. return true;
  109. }
  110. }
  111. }
  112. return false;
  113. }
  114. /**
  115. * Potential hydrogen acceptor
  116. */
  117. function addUnitHydrogenAcceptors(structure: Structure, unit: Unit.Atomic, builder: FeaturesBuilder) {
  118. const { charge, implicitH, idealGeometry } = getUnitValenceModel(structure, unit);
  119. const { elements } = unit;
  120. const { x, y, z } = unit.model.atomicConformation;
  121. const { elementAromaticRingIndices } = unit.rings;
  122. const add = (i: StructureElement.UnitIndex) => {
  123. builder.add(FeatureType.HydrogenAcceptor, FeatureGroup.None, x[elements[i]], y[elements[i]], z[elements[i]], i);
  124. };
  125. for (let i = 0 as StructureElement.UnitIndex, il = elements.length; i < il; ++i) {
  126. if (elementAromaticRingIndices.has(i)) continue;
  127. const element = typeSymbol(unit, i);
  128. if (element === Elements.O) {
  129. // Basically assume all oxygen atoms are acceptors!
  130. add(i);
  131. } else if (element === Elements.N) {
  132. if (isHistidineNitrogen(unit, i)) {
  133. // include both nitrogen atoms in histidine due to
  134. // their often ambiguous protonation assignment
  135. add(i);
  136. } else if (charge[i] < 1) {
  137. // Neutral nitrogen might be an acceptor
  138. // It must have at least one lone pair not conjugated
  139. const totalBonds = bondCount(structure, unit, i) + implicitH[i];
  140. const ig = idealGeometry[i];
  141. if (
  142. (ig === AtomGeometry.Tetrahedral && totalBonds < 4) ||
  143. (ig === AtomGeometry.Trigonal && totalBonds < 3) ||
  144. (ig === AtomGeometry.Linear && totalBonds < 2)
  145. ) {
  146. add(i);
  147. }
  148. }
  149. } else if (element === Elements.S) {
  150. const resname = compId(unit, i);
  151. if (resname === 'CYS' || resname === 'MET' || formalCharge(unit, i) === -1) {
  152. add(i);
  153. }
  154. }
  155. }
  156. }
  157. function isWater(unit: Unit.Atomic, index: StructureElement.UnitIndex) {
  158. return unit.model.atomicHierarchy.derived.residue.moleculeType[unit.residueIndex[unit.elements[index]]] === MoleculeType.Water;
  159. }
  160. function isBackbone(unit: Unit.Atomic, index: StructureElement.UnitIndex) {
  161. return ProteinBackboneAtoms.has(atomId(unit, index));
  162. }
  163. function isRing(unit: Unit.Atomic, index: StructureElement.UnitIndex) {
  164. return unit.rings.elementRingIndices.has(index);
  165. }
  166. function isHistidineNitrogen(unit: Unit.Atomic, index: StructureElement.UnitIndex) {
  167. return compId(unit, index) === 'HIS' && typeSymbol(unit, index) === Elements.N && isRing(unit, index);
  168. }
  169. function isBackboneHydrogenBond(unitA: Unit.Atomic, indexA: StructureElement.UnitIndex, unitB: Unit.Atomic, indexB: StructureElement.UnitIndex) {
  170. return isBackbone(unitA, indexA) && isBackbone(unitB, indexB);
  171. }
  172. function isWaterHydrogenBond(unitA: Unit.Atomic, indexA: StructureElement.UnitIndex, unitB: Unit.Atomic, indexB: StructureElement.UnitIndex) {
  173. return isWater(unitA, indexA) && isWater(unitB, indexB);
  174. }
  175. function isHydrogenBond(ti: FeatureType, tj: FeatureType) {
  176. return (
  177. (ti === FeatureType.HydrogenAcceptor && tj === FeatureType.HydrogenDonor) ||
  178. (ti === FeatureType.HydrogenDonor && tj === FeatureType.HydrogenAcceptor)
  179. );
  180. }
  181. function isWeakHydrogenBond(ti: FeatureType, tj: FeatureType) {
  182. return (
  183. (ti === FeatureType.WeakHydrogenDonor && tj === FeatureType.HydrogenAcceptor) ||
  184. (ti === FeatureType.HydrogenAcceptor && tj === FeatureType.WeakHydrogenDonor)
  185. );
  186. }
  187. function getGeometryOptions(props: GeometryProps) {
  188. return {
  189. includeBackbone: props.backbone,
  190. maxAccAngleDev: degToRad(props.accAngleDevMax),
  191. maxDonAngleDev: degToRad(props.donAngleDevMax),
  192. maxAccOutOfPlaneAngle: degToRad(props.accOutOfPlaneAngleMax),
  193. maxDonOutOfPlaneAngle: degToRad(props.donOutOfPlaneAngleMax),
  194. };
  195. }
  196. type GeometryOptions = ReturnType<typeof getGeometryOptions>
  197. function getHydrogenBondsOptions(props: HydrogenBondsProps) {
  198. return {
  199. ...getGeometryOptions(props),
  200. includeWater: props.water,
  201. maxSulfurDistSq: props.sulfurDistanceMax * props.sulfurDistanceMax,
  202. maxDistSq: props.distanceMax * props.distanceMax
  203. };
  204. }
  205. type HydrogenBondsOptions = ReturnType<typeof getHydrogenBondsOptions>
  206. const deg120InRad = degToRad(120);
  207. function checkGeometry(structure: Structure, don: Features.Info, acc: Features.Info, opts: GeometryOptions): true | undefined {
  208. const donIndex = don.members[don.offsets[don.feature]];
  209. const accIndex = acc.members[acc.offsets[acc.feature]];
  210. if (!opts.includeBackbone && isBackboneHydrogenBond(don.unit, donIndex, acc.unit, accIndex)) return;
  211. const donAngles = calcAngles(structure, don.unit, donIndex, acc.unit, accIndex);
  212. const idealDonAngle = AtomGeometryAngles.get(don.idealGeometry[donIndex]) || deg120InRad;
  213. if (donAngles.some(donAngle => Math.abs(idealDonAngle - donAngle) > opts.maxDonAngleDev)) return;
  214. if (don.idealGeometry[donIndex] === AtomGeometry.Trigonal) {
  215. const outOfPlane = calcPlaneAngle(structure, don.unit, donIndex, acc.unit, accIndex);
  216. if (outOfPlane !== undefined && outOfPlane > opts.maxDonOutOfPlaneAngle) return;
  217. }
  218. const accAngles = calcAngles(structure, acc.unit, accIndex, don.unit, donIndex);
  219. const idealAccAngle = AtomGeometryAngles.get(acc.idealGeometry[accIndex]) || deg120InRad;
  220. // Do not limit large acceptor angles
  221. if (accAngles.some(accAngle => idealAccAngle - accAngle > opts.maxAccAngleDev)) return;
  222. if (acc.idealGeometry[accIndex] === AtomGeometry.Trigonal) {
  223. const outOfPlane = calcPlaneAngle(structure, acc.unit, accIndex, don.unit, donIndex);
  224. if (outOfPlane !== undefined && outOfPlane > opts.maxAccOutOfPlaneAngle) return;
  225. }
  226. return true;
  227. }
  228. function testHydrogenBond(structure: Structure, infoA: Features.Info, infoB: Features.Info, distanceSq: number, opts: HydrogenBondsOptions): InteractionType | undefined {
  229. const typeA = infoA.types[infoA.feature];
  230. const typeB = infoB.types[infoB.feature];
  231. if (!isHydrogenBond(typeA, typeB)) return;
  232. const [don, acc] = typeB === FeatureType.HydrogenAcceptor ? [infoA, infoB] : [infoB, infoA];
  233. const donIndex = don.members[don.offsets[don.feature]];
  234. const accIndex = acc.members[acc.offsets[acc.feature]];
  235. // check if distance is ok depending on non-sulfur-containing hbond
  236. const maxDistSq = typeSymbol(don.unit, donIndex) === Elements.S || typeSymbol(acc.unit, accIndex) === Elements.S ? opts.maxSulfurDistSq : opts.maxDistSq;
  237. if (distanceSq > maxDistSq) return;
  238. if (!opts.includeWater && isWaterHydrogenBond(don.unit, donIndex, acc.unit, accIndex)) return;
  239. if (!checkGeometry(structure, don, acc, opts)) return;
  240. return InteractionType.HydrogenBond;
  241. }
  242. function testWeakHydrogenBond(structure: Structure, infoA: Features.Info, infoB: Features.Info, distanceSq: number, opts: GeometryOptions): InteractionType | undefined {
  243. const typeA = infoA.types[infoA.feature];
  244. const typeB = infoB.types[infoB.feature];
  245. if (!isWeakHydrogenBond(typeA, typeB)) return;
  246. const [don, acc] = typeB === FeatureType.HydrogenAcceptor ? [infoA, infoB] : [infoB, infoA];
  247. if (!checkGeometry(structure, don, acc, opts)) return;
  248. return InteractionType.WeakHydrogenBond;
  249. }
  250. //
  251. export const HydrogenDonorProvider = Features.Provider([FeatureType.HydrogenDonor], addUnitHydrogenDonors);
  252. export const WeakHydrogenDonorProvider = Features.Provider([FeatureType.WeakHydrogenDonor], addUnitWeakHydrogenDonors);
  253. export const HydrogenAcceptorProvider = Features.Provider([FeatureType.HydrogenAcceptor], addUnitHydrogenAcceptors);
  254. export const HydrogenBondsProvider: ContactProvider<HydrogenBondsParams> = {
  255. name: 'hydrogen-bonds',
  256. params: HydrogenBondsParams,
  257. createTester: (props: HydrogenBondsProps) => {
  258. const maxDistance = Math.max(props.distanceMax, props.sulfurDistanceMax);
  259. const opts = getHydrogenBondsOptions(props);
  260. return {
  261. maxDistance,
  262. requiredFeatures: new Set([FeatureType.HydrogenDonor, FeatureType.HydrogenAcceptor]),
  263. getType: (structure, infoA, infoB, distanceSq) => testHydrogenBond(structure, infoA, infoB, distanceSq, opts)
  264. };
  265. }
  266. };
  267. export const WeakHydrogenBondsProvider: ContactProvider<WeakHydrogenBondsParams> = {
  268. name: 'weak-hydrogen-bonds',
  269. params: WeakHydrogenBondsParams,
  270. createTester: (props: WeakHydrogenBondsProps) => {
  271. const opts = getGeometryOptions(props);
  272. return {
  273. maxDistance: props.distanceMax,
  274. requiredFeatures: new Set([FeatureType.WeakHydrogenDonor, FeatureType.HydrogenAcceptor]),
  275. getType: (structure, infoA, infoB, distanceSq) => testWeakHydrogenBond(structure, infoA, infoB, distanceSq, opts)
  276. };
  277. }
  278. };