valence-model.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. /**
  2. * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author Fred Ludlow <Fred.Ludlow@astx.com>
  5. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  6. */
  7. import { Structure, StructureElement, Unit, Bond } from '../../../mol-model/structure';
  8. import { Elements, isMetal } from '../../../mol-model/structure/model/properties/atomic/types';
  9. import { AtomGeometry, assignGeometry } from './geometry';
  10. import { bondCount, typeSymbol, formalCharge, bondToElementCount } from './util';
  11. import { ParamDefinition as PD } from '../../../mol-util/param-definition';
  12. import { RuntimeContext } from '../../../mol-task';
  13. import { isDebugMode } from '../../../mol-util/debug';
  14. import { SortedArray } from '../../../mol-data/int';
  15. /**
  16. * TODO:
  17. * Ensure proper treatment of disorder/models. e.g. V257 N in 5vim
  18. * Formal charge of 255 for SO4 anion (e.g. 5ghl)
  19. * Have removed a lot of explicit features (as I think they're more
  20. * generally captured by better VM).
  21. * Could we instead have a "delocalised negative/positive" charge
  22. * feature and flag these up?
  23. *
  24. */
  25. const tmpConjBondItA = new Bond.ElementBondIterator()
  26. const tmpConjBondItB = new Bond.ElementBondIterator()
  27. /**
  28. * Are we involved in some kind of pi system. Either explicitly forming
  29. * double bond or N, O next to a double bond, except:
  30. *
  31. * N,O with degree 4 cannot be conjugated.
  32. * N,O adjacent to P=O or S=O do not qualify (keeps sulfonamide N sp3 geom)
  33. */
  34. function isConjugated (structure: Structure, unit: Unit.Atomic, index: StructureElement.UnitIndex) {
  35. const element = typeSymbol(unit, index)
  36. const hetero = element === Elements.O || element === Elements.N
  37. if (hetero && bondCount(structure, unit, index) === 4) return false
  38. tmpConjBondItA.setElement(structure, unit, index)
  39. while (tmpConjBondItA.hasNext) {
  40. const bA = tmpConjBondItA.move()
  41. if (bA.order > 1) return true
  42. if (hetero) {
  43. const elementB = typeSymbol(bA.otherUnit, bA.otherIndex)
  44. tmpConjBondItB.setElement(structure, bA.otherUnit, bA.otherIndex)
  45. while (tmpConjBondItB.hasNext) {
  46. const bB = tmpConjBondItB.move()
  47. if (bB.order > 1) {
  48. if ((elementB === Elements.P || elementB === Elements.S) &&
  49. typeSymbol(bB.otherUnit, bB.otherIndex) === Elements.O) {
  50. continue
  51. }
  52. return true
  53. }
  54. }
  55. }
  56. }
  57. return false
  58. }
  59. export function explicitValence (structure: Structure, unit: Unit.Atomic, index: StructureElement.UnitIndex) {
  60. let v = 0
  61. // intra-unit bonds
  62. const { offset, edgeProps } = unit.bonds
  63. for (let i = offset[index], il = offset[index + 1]; i < il; ++i) v += edgeProps.order[i]
  64. // inter-unit bonds
  65. structure.interUnitBonds.getEdgeIndices(index, unit).forEach(b => v += structure.interUnitBonds.edges[b].props.order)
  66. return v
  67. }
  68. const tmpChargeBondItA = new Bond.ElementBondIterator()
  69. const tmpChargeBondItB = new Bond.ElementBondIterator()
  70. /**
  71. * Attempts to produce a consistent charge and implicit
  72. * H-count for an atom.
  73. *
  74. * If both props.assignCharge and props.assignH, this
  75. * approximately follows the rules described in
  76. * https://docs.eyesopen.com/toolkits/python/oechemtk/valence.html#openeye-hydrogen-count-model
  77. *
  78. * If only charge or hydrogens are to be assigned it takes
  79. * a much simpler view and deduces one from the other
  80. */
  81. export function calculateHydrogensCharge (structure: Structure, unit: Unit.Atomic, index: StructureElement.UnitIndex, props: ValenceModelProps) {
  82. const hydrogenCount = bondToElementCount(structure, unit, index, Elements.H)
  83. const element = typeSymbol(unit, index)
  84. let charge = formalCharge(unit, index)
  85. const assignCharge = (props.assignCharge === 'always' || (props.assignCharge === 'auto' && charge === 0))
  86. const assignH = (props.assignH === 'always' || (props.assignH === 'auto' && hydrogenCount === 0))
  87. const degree = bondCount(structure, unit, index)
  88. const valence = explicitValence(structure, unit, index)
  89. const conjugated = isConjugated(structure, unit, index)
  90. const multiBond = (valence - degree > 0)
  91. let implicitHCount = 0
  92. let geom = AtomGeometry.Unknown
  93. switch (element) {
  94. case Elements.H:
  95. if (assignCharge) {
  96. if (degree === 0) {
  97. charge = 1
  98. geom = AtomGeometry.Spherical
  99. } else if (degree === 1) {
  100. charge = 0
  101. geom = AtomGeometry.Terminal
  102. }
  103. }
  104. break
  105. case Elements.C:
  106. // TODO: Isocyanide?
  107. if (assignCharge) {
  108. charge = 0 // Assume carbon always neutral
  109. }
  110. if (assignH) {
  111. // Carbocation/carbanion are 3-valent
  112. implicitHCount = Math.max(0, 4 - valence - Math.abs(charge))
  113. }
  114. // Carbocation is planar, carbanion is tetrahedral
  115. geom = assignGeometry(degree + implicitHCount + Math.max(0, -charge))
  116. break
  117. case Elements.N:
  118. if (assignCharge) {
  119. if (!assignH) { // Trust input H explicitly:
  120. charge = valence - 3
  121. } else if (conjugated && valence < 4) {
  122. // Neutral unless amidine/guanidine double-bonded N:
  123. if (degree - hydrogenCount === 1 && valence - hydrogenCount === 2) {
  124. charge = 1
  125. } else {
  126. charge = 0
  127. }
  128. } else {
  129. // Sulfonamide nitrogen and classed as sp3 in conjugation model but
  130. // they won't be charged
  131. // Don't assign charge to nitrogens bound to metals
  132. tmpChargeBondItA.setElement(structure, unit, index)
  133. while (tmpChargeBondItA.hasNext) {
  134. const b = tmpChargeBondItA.move()
  135. const elementB = typeSymbol(b.otherUnit, b.otherIndex)
  136. if (elementB === Elements.S || isMetal(elementB)) {
  137. charge = 0
  138. break
  139. } else {
  140. charge = 1
  141. }
  142. }
  143. // TODO: Planarity sanity check?
  144. }
  145. }
  146. if (assignH) {
  147. // NH4+ -> 4, 1' amide -> 2, nitro N/N+ depiction -> 0
  148. implicitHCount = Math.max(0, 3 - valence + charge)
  149. }
  150. if (conjugated && !multiBond) {
  151. // Amide, anilinic N etc. cannot consider lone-pair for geometry purposes
  152. // Anilinic N geometry is depenent on ring electronics, for our purposes we
  153. // assume it's trigonal!
  154. geom = assignGeometry(degree + implicitHCount - charge)
  155. } else {
  156. // Everything else, pyridine, amine, nitrile, lp plays normal role:
  157. geom = assignGeometry(degree + implicitHCount + 1 - charge)
  158. }
  159. break
  160. case Elements.O:
  161. if (assignCharge) {
  162. if (!assignH) {
  163. charge = valence - 2
  164. }
  165. if (valence === 1) {
  166. tmpChargeBondItA.setElement(structure, unit, index)
  167. b1: while (tmpChargeBondItA.hasNext) {
  168. const bA = tmpChargeBondItA.move()
  169. tmpChargeBondItB.setElement(structure, bA.otherUnit, bA.otherIndex)
  170. while (tmpChargeBondItB.hasNext) {
  171. const bB = tmpChargeBondItB.move()
  172. if (
  173. !(bB.otherUnit === unit && bB.otherIndex === index) &&
  174. typeSymbol(bB.otherUnit, bB.otherIndex) === Elements.O &&
  175. bB.order === 2
  176. ) {
  177. charge = -1
  178. break b1
  179. }
  180. }
  181. }
  182. }
  183. }
  184. if (assignH) {
  185. // ethanol -> 1, carboxylate -> -1
  186. implicitHCount = Math.max(0, 2 - valence + charge)
  187. }
  188. if (conjugated && !multiBond) {
  189. // carboxylate OH, phenol OH, one lone-pair taken up with conjugation
  190. geom = assignGeometry(degree + implicitHCount - charge + 1)
  191. } else {
  192. // Carbonyl (trigonal)
  193. geom = assignGeometry(degree + implicitHCount - charge + 2)
  194. }
  195. break
  196. // Only handles thiols/thiolates/thioether/sulfonium. Sulfoxides and higher
  197. // oxidiation states are assumed neutral S (charge carried on O if required)
  198. case Elements.S:
  199. if (assignCharge) {
  200. if (!assignH) {
  201. if (valence <= 3 && bondToElementCount(structure, unit, index, Elements.O) === 0) {
  202. charge = valence - 2 // e.g. explicitly deprotonated thiol
  203. } else {
  204. charge = 0
  205. }
  206. }
  207. }
  208. if (assignH) {
  209. if (valence < 2) {
  210. implicitHCount = Math.max(0, 2 - valence + charge)
  211. }
  212. }
  213. if (valence <= 3) {
  214. // Thiol, thiolate, tioether -> tetrahedral
  215. geom = assignGeometry(degree + implicitHCount - charge + 2)
  216. }
  217. break
  218. case Elements.F:
  219. case Elements.CL:
  220. case Elements.BR:
  221. case Elements.I:
  222. case Elements.AT:
  223. // Never implicitly protonate halides
  224. if (assignCharge) {
  225. charge = valence - 1
  226. }
  227. break
  228. case Elements.LI:
  229. case Elements.NA:
  230. case Elements.K:
  231. case Elements.RB:
  232. case Elements.CS:
  233. case Elements.FR:
  234. if (assignCharge) {
  235. charge = 1 - valence
  236. }
  237. break
  238. case Elements.BE:
  239. case Elements.MG:
  240. case Elements.CA:
  241. case Elements.SR:
  242. case Elements.BA:
  243. case Elements.RA:
  244. if (assignCharge) {
  245. charge = 2 - valence
  246. }
  247. break
  248. default:
  249. if (isDebugMode) {
  250. console.warn('Requested charge, protonation for an unhandled element', element)
  251. }
  252. }
  253. return [ charge, implicitHCount, implicitHCount + hydrogenCount, geom ]
  254. }
  255. function calcUnitValenceModel(structure: Structure, unit: Unit.Atomic, props: ValenceModelProps) {
  256. const n = unit.elements.length
  257. const charge = new Int8Array(n)
  258. const implicitH = new Int8Array(n)
  259. const totalH = new Int8Array(n)
  260. const idealGeometry = new Int8Array(n)
  261. // always use root UnitIndex to take the topology of the whole structure in account
  262. const hasParent = !!structure.parent
  263. let mapping: SortedArray
  264. if (hasParent) {
  265. const rootUnit = structure.root.unitMap.get(unit.id) as Unit.Atomic
  266. mapping = SortedArray.indicesOf(rootUnit.elements, unit.elements)
  267. if (mapping.length !== unit.elements.length) {
  268. throw new Error('expected to find an index for every element')
  269. }
  270. unit = rootUnit
  271. structure = structure.root
  272. }
  273. for (let i = 0; i < n; ++i) {
  274. const j = (hasParent ? mapping![i] : i) as StructureElement.UnitIndex
  275. const [ chg, implH, totH, geom ] = calculateHydrogensCharge(structure, unit, j, props)
  276. charge[i] = chg
  277. implicitH[i] = implH
  278. totalH[i] = totH
  279. idealGeometry[i] = geom
  280. }
  281. return { charge, implicitH, totalH, idealGeometry }
  282. }
  283. export interface ValenceModel {
  284. charge: Int8Array,
  285. implicitH: Int8Array,
  286. totalH: Int8Array,
  287. idealGeometry: Int8Array
  288. }
  289. export const ValenceModelParams = {
  290. assignCharge: PD.Select('auto', [['always', 'always'], ['auto', 'auto'], ['never', 'never']]),
  291. assignH: PD.Select('auto', [['always', 'always'], ['auto', 'auto'], ['never', 'never']]),
  292. }
  293. export type ValenceModelParams = typeof ValenceModelParams
  294. export type ValenceModelProps = PD.Values<ValenceModelParams>
  295. export async function calcValenceModel(ctx: RuntimeContext, structure: Structure, props: Partial<ValenceModelProps>) {
  296. const p = { ...PD.getDefaultValues(ValenceModelParams), ...props }
  297. const map = new Map<number, ValenceModel>()
  298. for (let i = 0, il = structure.units.length; i < il; ++i) {
  299. const u = structure.units[i]
  300. if (Unit.isAtomic(u)) {
  301. const valenceModel = calcUnitValenceModel(structure, u, p)
  302. map.set(u.id, valenceModel)
  303. }
  304. }
  305. return map
  306. }