valence-model.ts 13 KB

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