secondary-structure.ts 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970
  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 { SecondaryStructure } from 'mol-model/structure/model/properties/seconday-structure';
  7. import { ResidueIndex } from 'mol-model/structure';
  8. import { MoleculeType, SecondaryStructureType } from 'mol-model/structure/model/types';
  9. import { Vec3 } from 'mol-math/linear-algebra';
  10. import { GridLookup3D } from 'mol-math/geometry';
  11. import { SortedArray } from 'mol-data/int';
  12. import { IntAdjacencyGraph } from 'mol-math/graph';
  13. import { BitFlags } from 'mol-util';
  14. import { ElementIndex } from 'mol-model/structure/model/indexing';
  15. import { AtomicHierarchy, AtomicConformation } from '../atomic';
  16. /**
  17. * TODO bugs to fix:
  18. * - some turns are not detected correctly: see e.g. pdb:1acj - maybe more than 2 hbonds require some residue to donate electrons
  19. * - some sheets are not extended correctly: see e.g. pdb:1acj
  20. * - validate new helix definition
  21. * - validate new ordering of secondary structure elements
  22. */
  23. export function computeSecondaryStructure(hierarchy: AtomicHierarchy,
  24. conformation: AtomicConformation): SecondaryStructure {
  25. // TODO use Zhang-Skolnik for CA alpha only parts or for coarse parts with per-residue elements
  26. return computeModelDSSP(hierarchy, conformation)
  27. }
  28. export function computeModelDSSP(hierarchy: AtomicHierarchy,
  29. conformation: AtomicConformation,
  30. oldDefinition = true,
  31. oldOrdering = true) {
  32. // console.log(`calculating secondary structure elements using ${ oldDefinition ? 'old' : 'revised'} definition and ${ oldOrdering ? 'old' : 'revised'} ordering of secondary structure elements`)
  33. const { lookup3d, proteinResidues } = calcAtomicTraceLookup3D(hierarchy, conformation)
  34. const backboneIndices = calcBackboneAtomIndices(hierarchy, proteinResidues)
  35. const hbonds = calcBackboneHbonds(hierarchy, conformation, proteinResidues, backboneIndices, lookup3d)
  36. const residueCount = proteinResidues.length
  37. const flags = new Uint32Array(residueCount)
  38. const torsionAngles = calculateDihedralAngles(hierarchy, conformation, proteinResidues, backboneIndices)
  39. const ladders: Ladder[] = []
  40. const bridges: Bridge[] = []
  41. const getResidueFlag = oldOrdering ? getOriginalResidueFlag : getUpdatedResidueFlag
  42. const getFlagName = oldOrdering ? getOriginalFlagName : getUpdatedFlagName
  43. const ctx: DSSPContext = {
  44. oldDefinition,
  45. oldOrdering,
  46. getResidueFlag,
  47. getFlagName,
  48. hierarchy,
  49. proteinResidues,
  50. flags,
  51. hbonds,
  52. torsionAngles,
  53. backboneIndices,
  54. conformation,
  55. ladders,
  56. bridges
  57. }
  58. assignTurns(ctx)
  59. assignHelices(ctx)
  60. assignBends(ctx)
  61. assignBridges(ctx)
  62. assignLadders(ctx)
  63. assignSheets(ctx)
  64. const assignment = getDSSPAssignment(flags, getResidueFlag)
  65. const type = new Uint32Array(hierarchy.residues._rowCount) as unknown as SecondaryStructureType[]
  66. const keys: number[] = []
  67. const elements: SecondaryStructure.Element[] = []
  68. for (let i = 0, il = proteinResidues.length; i < il; ++i) {
  69. const assign = assignment[i]
  70. type[proteinResidues[i]] = assign
  71. const flag = getResidueFlag(flags[i])
  72. // const kind = mapToKind(assign)
  73. // TODO is this expected behavior? elements will be strictly split depending on 'winning' flag
  74. if (elements.length === 0 || // check ought to fail at very start
  75. // elements[elements.length - 1].kind !== kind || // new element if overall kind changed
  76. flag !== (elements[elements.length - 1] as SecondaryStructure.Helix | SecondaryStructure.Sheet).flags) { // exact flag changed
  77. elements[elements.length] = createElement(mapToKind(assign), flags[i], getResidueFlag)
  78. }
  79. keys[i] = elements.length - 1
  80. }
  81. const secondaryStructure: SecondaryStructure = {
  82. type,
  83. key: keys,
  84. elements: elements
  85. }
  86. // console.log(keys)
  87. // console.log(elements)
  88. // printDSSPString(flags, getFlagName)
  89. return secondaryStructure
  90. }
  91. // function printDSSPString(flags: Uint32Array, getFlagName: (f: DSSPType) => String) {
  92. // let out = ''
  93. // for (let i = 0, il = flags.length; i < il; ++i) {
  94. // const f = DSSPType.create(flags[i])
  95. // out += getFlagName(f)
  96. // }
  97. // console.log(out)
  98. // }
  99. function createElement(kind: string, flag: DSSPType.Flag, getResidueFlag: (f: DSSPType) => SecondaryStructureType): SecondaryStructure.Element {
  100. // TODO would be nice to add more detailed information
  101. if (kind === 'helix') {
  102. return {
  103. kind: 'helix',
  104. flags: getResidueFlag(flag)
  105. } as SecondaryStructure.Helix
  106. } else if (kind === 'sheet') {
  107. return {
  108. kind: 'sheet',
  109. flags: getResidueFlag(flag)
  110. } as SecondaryStructure.Sheet
  111. } else {
  112. return {
  113. kind: 'none',
  114. flags: getResidueFlag(flag)
  115. }
  116. }
  117. }
  118. function mapToKind(assignment: SecondaryStructureType.Flag) {
  119. if (assignment === SecondaryStructureType.SecondaryStructureDssp.H || assignment === SecondaryStructureType.SecondaryStructureDssp.G ||
  120. assignment === SecondaryStructureType.SecondaryStructureDssp.I) {
  121. return 'helix'
  122. } else if (assignment === SecondaryStructureType.SecondaryStructureDssp.B || assignment === SecondaryStructureType.SecondaryStructureDssp.E) {
  123. return 'sheet'
  124. } else {
  125. return 'none'
  126. }
  127. }
  128. interface DSSPContext {
  129. /** Whether to use the old DSSP convention for the annotation of turns and helices, causes them to be two residues shorter */
  130. oldDefinition: boolean,
  131. /** alpha-helices are preferred over 3-10 helix */
  132. oldOrdering: boolean,
  133. getResidueFlag: (f: DSSPType) => SecondaryStructureType,
  134. getFlagName: (f: DSSPType) => String,
  135. hierarchy: AtomicHierarchy
  136. proteinResidues: SortedArray<ResidueIndex>
  137. /** flags for each residue */
  138. flags: Uint32Array
  139. hbonds: DsspHbonds,
  140. torsionAngles: { phi: number[], psi: number[], omega: number[] },
  141. backboneIndices: BackboneAtomIndices,
  142. conformation: AtomicConformation,
  143. ladders: Ladder[],
  144. bridges: Bridge[]
  145. }
  146. interface Ladder {
  147. previousLadder: number,
  148. nextLadder: number,
  149. firstStart: number,
  150. secondStart: number,
  151. secondEnd: number,
  152. firstEnd: number,
  153. type: BridgeType
  154. }
  155. const enum BridgeType {
  156. PARALLEL = 0x0,
  157. ANTI_PARALLEL = 0x1
  158. }
  159. interface Bridge {
  160. partner1: number,
  161. partner2: number,
  162. type: BridgeType
  163. }
  164. type DSSPType = BitFlags<DSSPType.Flag>
  165. namespace DSSPType {
  166. export const is: (t: DSSPType, f: Flag) => boolean = BitFlags.has
  167. export const create: (f: Flag) => DSSPType = BitFlags.create
  168. export const enum Flag {
  169. _ = 0x0,
  170. H = 0x1,
  171. B = 0x2,
  172. E = 0x4,
  173. G = 0x8,
  174. I = 0x10,
  175. S = 0x20,
  176. T = 0x40,
  177. T3 = 0x80,
  178. T4 = 0x100,
  179. T5 = 0x200,
  180. T3S = 0x400, // marks 3-turn start
  181. T4S = 0x800,
  182. T5S = 0x1000
  183. }
  184. }
  185. /** max distance between two C-alpha atoms to check for hbond */
  186. const caMaxDist = 7.0;
  187. /** min distance between two C-alpha atoms to check for hbond */
  188. const caMinDist = 0.5;
  189. function calcAtomicTraceLookup3D(hierarchy: AtomicHierarchy, conformation: AtomicConformation) {
  190. const { x, y, z } = conformation;
  191. const { moleculeType, traceElementIndex } = hierarchy.derived.residue
  192. const indices: number[] = []
  193. const _proteinResidues: number[] = []
  194. for (let i = 0, il = moleculeType.length; i < il; ++i) {
  195. if (moleculeType[i] === MoleculeType.protein) {
  196. indices[indices.length] = traceElementIndex[i]
  197. _proteinResidues[_proteinResidues.length] = i
  198. }
  199. }
  200. const lookup3d = GridLookup3D({ x, y, z, indices: SortedArray.ofSortedArray(indices) }, 4);
  201. const proteinResidues = SortedArray.ofSortedArray<ResidueIndex>(_proteinResidues)
  202. return { lookup3d, proteinResidues }
  203. }
  204. interface BackboneAtomIndices {
  205. cIndices: ArrayLike<ElementIndex | -1>
  206. hIndices: ArrayLike<ElementIndex | -1>
  207. oIndices: ArrayLike<ElementIndex | -1>
  208. nIndices: ArrayLike<ElementIndex | -1>
  209. }
  210. function calcBackboneAtomIndices(hierarchy: AtomicHierarchy, proteinResidues: SortedArray<ResidueIndex>): BackboneAtomIndices {
  211. const residueCount = proteinResidues.length
  212. const { index } = hierarchy
  213. const c = new Int32Array(residueCount)
  214. const h = new Int32Array(residueCount)
  215. const o = new Int32Array(residueCount)
  216. const n = new Int32Array(residueCount)
  217. for (let i = 0, il = residueCount; i < il; ++i) {
  218. const rI = proteinResidues[i]
  219. c[i] = index.findAtomOnResidue(rI, 'C')
  220. h[i] = index.findAtomOnResidue(rI, 'H')
  221. o[i] = index.findAtomOnResidue(rI, 'O')
  222. n[i] = index.findAtomOnResidue(rI, 'N')
  223. }
  224. return {
  225. cIndices: c as unknown as ArrayLike<ElementIndex | -1>,
  226. hIndices: h as unknown as ArrayLike<ElementIndex | -1>,
  227. oIndices: o as unknown as ArrayLike<ElementIndex | -1>,
  228. nIndices: n as unknown as ArrayLike<ElementIndex | -1>,
  229. }
  230. }
  231. type DsspHbonds = IntAdjacencyGraph<{ readonly energies: ArrayLike<number> }>
  232. /**
  233. * Bend(i) =: [angle ((CW - Ca(i - 2)),(C"(i + 2) - C"(i))) > 70"]
  234. *
  235. * Type: S
  236. */
  237. function assignBends(ctx: DSSPContext) {
  238. const flags = ctx.flags
  239. const { x, y, z } = ctx.conformation
  240. const { traceElementIndex } = ctx.hierarchy.derived.residue
  241. const proteinResidues = ctx.proteinResidues
  242. const residueCount = proteinResidues.length
  243. const position = (i: number, v: Vec3) => Vec3.set(v, x[i], y[i], z[i])
  244. const caPosPrev2 = Vec3.zero()
  245. const caPos = Vec3.zero()
  246. const caPosNext2 = Vec3.zero()
  247. // let bends = 0
  248. const nIndices = ctx.backboneIndices.nIndices
  249. const cPos = Vec3.zero()
  250. const nPosNext = Vec3.zero()
  251. f1: for (let i = 2; i < residueCount - 2; i++) {
  252. // check for peptide bond
  253. for (let k = 0; k < 4; k++) {
  254. let index = i + k - 2
  255. position(traceElementIndex[index], cPos)
  256. position(nIndices[index + 1], nPosNext)
  257. if (Vec3.squaredDistance(cPos, nPosNext) > 6.25 /* max squared peptide bond distance allowed */) {
  258. continue f1
  259. }
  260. }
  261. const oRIprev2 = proteinResidues[i - 2]
  262. const oRI = proteinResidues[i]
  263. const oRInext2 = proteinResidues[i + 2]
  264. const caAtomPrev2 = traceElementIndex[oRIprev2]
  265. const caAtom = traceElementIndex[oRI]
  266. const caAtomNext2 = traceElementIndex[oRInext2]
  267. position(caAtomPrev2, caPosPrev2)
  268. position(caAtom, caPos)
  269. position(caAtomNext2, caPosNext2)
  270. const caMinus2 = Vec3.zero()
  271. const caPlus2 = Vec3.zero()
  272. Vec3.sub(caMinus2, caPosPrev2, caPos)
  273. Vec3.sub(caPlus2, caPos, caPosNext2)
  274. const angle = Vec3.angle(caMinus2, caPlus2) * 360 / (2 * Math.PI)
  275. if (angle && angle > 70.00) {
  276. // console.log(`found bend at ${ i } with kappa ${ angle }`)
  277. flags[i] |= DSSPType.Flag.S
  278. // bends++
  279. }
  280. }
  281. // console.log(bends + ' bends')
  282. }
  283. function calculateDihedralAngles(hierarchy: AtomicHierarchy, conformation: AtomicConformation, proteinResidues: SortedArray<ResidueIndex>, backboneIndices: BackboneAtomIndices): { phi: number[], psi: number[], omega: number[] } {
  284. const { cIndices, nIndices } = backboneIndices
  285. const { x, y, z } = conformation
  286. const { traceElementIndex } = hierarchy.derived.residue
  287. const residueCount = proteinResidues.length
  288. const position = (i: number, v: Vec3) => Vec3.set(v, x[i], y[i], z[i])
  289. const cPosPrev = Vec3.zero()
  290. const caPosPrev = Vec3.zero()
  291. const nPosPrev = Vec3.zero()
  292. const cPos = Vec3.zero()
  293. const caPos = Vec3.zero()
  294. const nPos = Vec3.zero()
  295. const cPosNext = Vec3.zero()
  296. const caPosNext = Vec3.zero()
  297. const nPosNext = Vec3.zero()
  298. const phi: number[] = []
  299. const psi: number[] = []
  300. const omega: number[] = []
  301. for (let i = 0; i < residueCount - 1; ++i) {
  302. const oPIprev = i - 1
  303. const oRIprev = proteinResidues[i - 1]
  304. const oPI = i
  305. const oRI = proteinResidues[i]
  306. const oPInext = i + 1
  307. const oRInext = proteinResidues[i + 1]
  308. const cAtomPrev = cIndices[oPIprev]
  309. const caAtomPrev = traceElementIndex[oRIprev]
  310. const nAtomPrev = nIndices[oPIprev]
  311. const cAtom = cIndices[oPI]
  312. const caAtom = traceElementIndex[oRI]
  313. const nAtom = nIndices[oPI]
  314. const cAtomNext = cIndices[oPInext]
  315. const caAtomNext = traceElementIndex[oRInext]
  316. const nAtomNext = nIndices[oRInext]
  317. // ignore C-terminal residue as acceptor
  318. // if (index.findAtomOnResidue(oRI, 'OXT') !== -1) continue
  319. position(cAtomPrev, cPosPrev)
  320. position(caAtomPrev, caPosPrev)
  321. position(nAtomPrev, nPosPrev)
  322. position(cAtom, cPos)
  323. position(caAtom, caPos)
  324. position(nAtom, nPos)
  325. position(cAtomNext, cPosNext)
  326. position(caAtomNext, caPosNext)
  327. position(nAtomNext, nPosNext)
  328. const tPhi = calculatePhi(cPosPrev, nPos, caPos, cPos)
  329. const tPsi = calculatePsi(nPos, caPos, cPos, nPosNext)
  330. const tOmega = calculateOmega(caPos, cPos, nPosNext, caPosNext)
  331. // console.log(`phi: ${ tPhi }, psi: ${ tPsi }, omega: ${ tOmega }`)
  332. phi[phi.length] = tPhi
  333. psi[psi.length] = tPsi
  334. omega[omega.length] = tOmega
  335. }
  336. return { phi, psi, omega };
  337. }
  338. // angle calculations return NaN upon missing atoms
  339. function calculatePhi(c: Vec3, nNext: Vec3, caNext: Vec3, cNext: Vec3) {
  340. return angle(c, nNext, caNext, cNext);
  341. }
  342. function calculatePsi(n: Vec3, ca: Vec3, c: Vec3, nNext: Vec3) {
  343. return angle(n, ca, c, nNext);
  344. }
  345. function calculateOmega(ca: Vec3, c: Vec3, nNext: Vec3, caNext: Vec3) {
  346. return angle(ca, c, nNext, caNext);
  347. }
  348. function calcBackboneHbonds(hierarchy: AtomicHierarchy, conformation: AtomicConformation, proteinResidues: SortedArray<ResidueIndex>, backboneIndices: BackboneAtomIndices, lookup3d: GridLookup3D): DsspHbonds {
  349. const { cIndices, hIndices, nIndices, oIndices } = backboneIndices
  350. const { index } = hierarchy
  351. const { x, y, z } = conformation
  352. const { traceElementIndex } = hierarchy.derived.residue
  353. const residueCount = proteinResidues.length
  354. const position = (i: number, v: Vec3) => Vec3.set(v, x[i], y[i], z[i])
  355. const oAtomResidues: number[] = [];
  356. const nAtomResidues: number[] = [];
  357. const energies: number[] = [];
  358. const oPos = Vec3.zero()
  359. const cPos = Vec3.zero()
  360. const caPos = Vec3.zero()
  361. const nPos = Vec3.zero()
  362. const hPos = Vec3.zero()
  363. const cPosPrev = Vec3.zero()
  364. const oPosPrev = Vec3.zero()
  365. // const caMinDistSq = caMinDist * caMinDist
  366. for (let i = 0, il = proteinResidues.length; i < il; ++i) {
  367. const oPI = i
  368. const oRI = proteinResidues[i]
  369. const oAtom = oIndices[oPI]
  370. const cAtom = cIndices[oPI]
  371. const caAtom = traceElementIndex[oRI]
  372. // continue if residue is missing O or C atom
  373. if (oAtom === -1 || cAtom === -1) continue
  374. // ignore C-terminal residue as acceptor
  375. if (index.findAtomOnResidue(oRI, 'OXT') !== -1) continue
  376. position(oAtom, oPos)
  377. position(cAtom, cPos)
  378. position(caAtom, caPos)
  379. const { indices, count } = lookup3d.find(caPos[0], caPos[1], caPos[2], caMaxDist)
  380. for (let j = 0; j < count; ++j) {
  381. // if (squaredDistances[j] < caMinDistSq) continue
  382. const nPI = indices[j]
  383. // ignore bonds within a residue or to prev or next residue, TODO take chain border into account
  384. if (nPI === oPI || nPI - 1 === oPI || nPI + 1 === oPI) continue
  385. const nAtom = nIndices[nPI]
  386. if (nAtom === -1) continue
  387. position(nAtom, nPos)
  388. const hAtom = hIndices[nPI]
  389. if (hAtom === -1) {
  390. // approximate calculation of H position, TODO factor out
  391. if (nPI === 0) continue
  392. const nPIprev = nPI - 1
  393. const oAtomPrev = oIndices[nPIprev]
  394. const cAtomPrev = cIndices[nPIprev]
  395. if (oAtomPrev === -1 || cAtomPrev === -1) continue
  396. position(oAtomPrev, oPosPrev)
  397. position(cAtomPrev, cPosPrev)
  398. Vec3.sub(hPos, cPosPrev, oPosPrev)
  399. const dist = Vec3.distance(oPosPrev, cPosPrev)
  400. Vec3.scaleAndAdd(hPos, nPos, hPos, 1 / dist)
  401. } else {
  402. position(hAtom, hPos)
  403. }
  404. const e = calcHbondEnergy(oPos, cPos, nPos, hPos)
  405. if (e > hbondEnergyCutoff) continue
  406. // console.log(`detected hbond between ${ oPI } and ${ nPI } with energy ${ e }`)
  407. oAtomResidues[oAtomResidues.length] = oPI
  408. nAtomResidues[nAtomResidues.length] = nPI
  409. energies[energies.length] = e
  410. }
  411. }
  412. return buildHbondGraph(residueCount, oAtomResidues, nAtomResidues, energies);
  413. }
  414. function buildHbondGraph(residueCount: number, oAtomResidues: number[], nAtomResidues: number[], energies: number[]) {
  415. const builder = new IntAdjacencyGraph.DirectedEdgeBuilder(residueCount, oAtomResidues, nAtomResidues);
  416. const _energies = new Float32Array(builder.slotCount);
  417. for (let i = 0, _i = builder.edgeCount; i < _i; i++) {
  418. builder.addNextEdge();
  419. builder.assignProperty(_energies, energies[i]);
  420. }
  421. return builder.createGraph({ energies });
  422. }
  423. /** Original priority: H,B,E,G,I,T,S */
  424. function getOriginalResidueFlag(f: DSSPType) {
  425. if (DSSPType.is(f, DSSPType.Flag.H)) return SecondaryStructureType.SecondaryStructureDssp.H
  426. if (DSSPType.is(f, DSSPType.Flag.E)) return SecondaryStructureType.SecondaryStructureDssp.E
  427. if (DSSPType.is(f, DSSPType.Flag.B)) return SecondaryStructureType.SecondaryStructureDssp.B
  428. if (DSSPType.is(f, DSSPType.Flag.G)) return SecondaryStructureType.SecondaryStructureDssp.G
  429. if (DSSPType.is(f, DSSPType.Flag.I)) return SecondaryStructureType.SecondaryStructureDssp.I
  430. if (DSSPType.is(f, DSSPType.Flag.T)) return SecondaryStructureType.SecondaryStructureDssp.T
  431. if (DSSPType.is(f, DSSPType.Flag.S)) return SecondaryStructureType.SecondaryStructureDssp.S
  432. return SecondaryStructureType.Flag.None
  433. }
  434. function getOriginalFlagName(f: DSSPType) {
  435. if (DSSPType.is(f, DSSPType.Flag.H)) return 'H'
  436. if (DSSPType.is(f, DSSPType.Flag.E)) return 'E'
  437. if (DSSPType.is(f, DSSPType.Flag.B)) return 'B'
  438. if (DSSPType.is(f, DSSPType.Flag.G)) return 'G'
  439. if (DSSPType.is(f, DSSPType.Flag.I)) return 'I'
  440. if (DSSPType.is(f, DSSPType.Flag.T)) return 'T'
  441. if (DSSPType.is(f, DSSPType.Flag.S)) return 'S'
  442. return '-'
  443. }
  444. /** Version 2.1.0 priority: I,H,B,E,G,T,S */
  445. function getUpdatedResidueFlag(f: DSSPType) {
  446. if (DSSPType.is(f, DSSPType.Flag.I)) return SecondaryStructureType.SecondaryStructureDssp.I
  447. if (DSSPType.is(f, DSSPType.Flag.H)) return SecondaryStructureType.SecondaryStructureDssp.H
  448. if (DSSPType.is(f, DSSPType.Flag.E)) return SecondaryStructureType.SecondaryStructureDssp.E
  449. if (DSSPType.is(f, DSSPType.Flag.B)) return SecondaryStructureType.SecondaryStructureDssp.B
  450. if (DSSPType.is(f, DSSPType.Flag.G)) return SecondaryStructureType.SecondaryStructureDssp.G
  451. if (DSSPType.is(f, DSSPType.Flag.T)) return SecondaryStructureType.SecondaryStructureDssp.T
  452. if (DSSPType.is(f, DSSPType.Flag.S)) return SecondaryStructureType.SecondaryStructureDssp.S
  453. return SecondaryStructureType.Flag.None
  454. }
  455. function getUpdatedFlagName(f: DSSPType) {
  456. if (DSSPType.is(f, DSSPType.Flag.I)) return 'I'
  457. if (DSSPType.is(f, DSSPType.Flag.H)) return 'H'
  458. if (DSSPType.is(f, DSSPType.Flag.E)) return 'E'
  459. if (DSSPType.is(f, DSSPType.Flag.B)) return 'B'
  460. if (DSSPType.is(f, DSSPType.Flag.G)) return 'G'
  461. if (DSSPType.is(f, DSSPType.Flag.T)) return 'T'
  462. if (DSSPType.is(f, DSSPType.Flag.S)) return 'S'
  463. return '-'
  464. }
  465. function getDSSPAssignment(flags: Uint32Array, getResidueFlag: (f: DSSPType) => SecondaryStructureType) {
  466. const type = new Uint32Array(flags.length)
  467. for (let i = 0, il = flags.length; i < il; ++i) {
  468. const f = DSSPType.create(flags[i])
  469. type[i] = getResidueFlag(f)
  470. }
  471. return type as unknown as ArrayLike<SecondaryStructureType>
  472. }
  473. /**
  474. * Constant for electrostatic energy in kcal/mol
  475. * f * q1 * q2
  476. * Q = -332 * 0.42 * 0.20
  477. *
  478. * f is the dimensional factor
  479. *
  480. * q1 and q2 are partial charges which are placed on the C,O
  481. * (+q1,-q1) and N,H (-q2,+q2)
  482. */
  483. const Q = -27.888
  484. /** cutoff for hbonds in kcal/mol, must be lower to be consider as an hbond */
  485. const hbondEnergyCutoff = -0.5
  486. const hbondEnergyMinimal = -9.9
  487. /**
  488. * E = Q * (1/r(ON) + l/r(CH) - l/r(OH) - l/r(CN))
  489. */
  490. function calcHbondEnergy(oPos: Vec3, cPos: Vec3, nPos: Vec3, hPos: Vec3) {
  491. const distOH = Vec3.distance(oPos, hPos)
  492. const distCH = Vec3.distance(cPos, hPos)
  493. const distCN = Vec3.distance(cPos, nPos)
  494. const distON = Vec3.distance(oPos, nPos)
  495. if (distOH < caMinDist || distCH < caMinDist || distCN < caMinDist || distON < caMinDist) {
  496. return hbondEnergyMinimal
  497. }
  498. const e1 = Q / distOH - Q / distCH
  499. const e2 = Q / distCN - Q / distON
  500. const e = e1 + e2
  501. // cap lowest possible energy
  502. if (e < hbondEnergyMinimal)
  503. return hbondEnergyMinimal
  504. return e
  505. }
  506. /**
  507. * The basic turn pattern is a single H bond of type (i, i + n).
  508. * We assign an n-turn at residue i if there is an H bond from CO(i) to NH(i + n),
  509. * i.e., “n-turn(i)=: Hbond(i, i + n), n = 3, 4, 5.”
  510. *
  511. * Type: T
  512. */
  513. function assignTurns(ctx: DSSPContext) {
  514. const { proteinResidues, hbonds, flags, hierarchy } = ctx
  515. const { chains, residueAtomSegments, chainAtomSegments } = hierarchy
  516. const { label_asym_id } = chains
  517. // let turns = 0, turnOpenings = 0
  518. const turnFlag = [DSSPType.Flag.T3S, DSSPType.Flag.T4S, DSSPType.Flag.T5S, DSSPType.Flag.T3, DSSPType.Flag.T4, DSSPType.Flag.T5]
  519. for (let idx = 0; idx < 3; idx++) {
  520. for (let i = 0, il = proteinResidues.length - 1; i < il; ++i) {
  521. const rI = proteinResidues[i]
  522. const cI = chainAtomSegments.index[residueAtomSegments.offsets[rI]]
  523. // TODO should take sequence gaps into account
  524. const rN = proteinResidues[i + idx + 3]
  525. const cN = chainAtomSegments.index[residueAtomSegments.offsets[rN]]
  526. // check if on same chain
  527. if (!label_asym_id.areValuesEqual(cI, cN)) continue
  528. // check if hbond exists
  529. // console.log(`${ i } has ${ idx + 3}-turn opening: ${ hbonds.getDirectedEdgeIndex(i, i + idx + 3) !== -1 }`)
  530. if (hbonds.getDirectedEdgeIndex(i, i + idx + 3) !== -1) {
  531. // console.log(`${ idx + 3 }-turn opening at ${ i } to ${ i + idx + 3 }`)
  532. flags[i] |= turnFlag[idx + 3] | turnFlag[idx]
  533. // turnOpenings++
  534. if (ctx.oldDefinition) {
  535. for (let k = 1; k < idx + 3; ++k) {
  536. flags[i + k] |= turnFlag[idx + 3] | DSSPType.Flag.T
  537. // turns++
  538. }
  539. } else {
  540. for (let k = 0; k <= idx + 3; ++k) {
  541. flags[i + k] |= turnFlag[idx + 3] | DSSPType.Flag.T
  542. // turns++
  543. }
  544. }
  545. }
  546. }
  547. }
  548. // console.log(`${ turns } turns, ${ turnOpenings } turn openings`)
  549. }
  550. /**
  551. * Two nonoverlapping stretches of three residues each, i - 1, i, i + 1 and j - 1, j, j + 1,
  552. * form either a parallel or antiparallel bridge, depending on which of
  553. * two basic patterns is matched. We assign a bridge between residues i and j
  554. * if there are two H bonds characteristic of P-structure; in particular,
  555. *
  556. * Parallel Bridge(i, j) =:
  557. * [Hbond(i - 1, j) and Hbond(j, i + 1)] or
  558. * [Hbond(j - 1, i) and Hbond(i, j + 1)]
  559. *
  560. * Antiparallel Bridge(i, j) =:
  561. * [Hbond(i, j) and Hbond(j, i)] or
  562. * [Hbond(i - 1, j + 1) and Hbond(j - 1, i + l)]
  563. *
  564. * Type: B
  565. */
  566. function assignBridges(ctx: DSSPContext) {
  567. const { proteinResidues, hbonds, flags, bridges } = ctx
  568. const { offset, b } = hbonds
  569. let i: number, j: number
  570. for (let k = 0, kl = proteinResidues.length; k < kl; ++k) {
  571. for (let t = offset[k], _t = offset[k + 1]; t < _t; t++) {
  572. const l = b[t]
  573. if (k > l) continue
  574. // Parallel Bridge(i, j) =: [Hbond(i - 1, j) and Hbond(j, i + 1)]
  575. i = k + 1 // k is i - 1
  576. j = l
  577. if (i !== j && hbonds.getDirectedEdgeIndex(j, i + 1) !== -1) {
  578. flags[i] |= DSSPType.Flag.B
  579. flags[j] |= DSSPType.Flag.B
  580. // TODO move to constructor, actually omit object all together
  581. bridges[bridges.length] = { partner1: Math.min(i, j), partner2: Math.max(i, j), type: BridgeType.PARALLEL }
  582. }
  583. // Parallel Bridge(i, j) =: [Hbond(j - 1, i) and Hbond(i, j + 1)]
  584. i = k
  585. j = l - 1 // l is j + 1
  586. if (i !== j && hbonds.getDirectedEdgeIndex(j - 1, i) !== -1) {
  587. flags[i] |= DSSPType.Flag.B
  588. flags[j] |= DSSPType.Flag.B
  589. bridges[bridges.length] = { partner1: Math.min(j, i), partner2: Math.max(j, i), type: BridgeType.PARALLEL }
  590. }
  591. // Antiparallel Bridge(i, j) =: [Hbond(i, j) and Hbond(j, i)]
  592. i = k
  593. j = l
  594. if (i !== j && hbonds.getDirectedEdgeIndex(j, i) !== -1) {
  595. flags[i] |= DSSPType.Flag.B
  596. flags[j] |= DSSPType.Flag.B
  597. bridges[bridges.length] = { partner1: Math.min(j, i), partner2: Math.max(j, i), type: BridgeType.ANTI_PARALLEL }
  598. }
  599. // Antiparallel Bridge(i, j) =: [Hbond(i - 1, j + 1) and Hbond(j - 1, i + l)]
  600. i = k + 1
  601. j = l - 1
  602. if (i !== j && hbonds.getDirectedEdgeIndex(j - 1, i + 1) !== -1) {
  603. flags[i] |= DSSPType.Flag.B
  604. flags[j] |= DSSPType.Flag.B
  605. bridges[bridges.length] = { partner1: Math.min(j, i), partner2: Math.max(j, i), type: BridgeType.ANTI_PARALLEL }
  606. }
  607. }
  608. }
  609. bridges.sort((a, b) => a.partner1 > b.partner1 ? 1 : a.partner1 < b.partner1 ? -1 : 0)
  610. // console.log(`${ bridges.length } bridges`)
  611. // bridges.forEach(b => console.log(b))
  612. }
  613. /**
  614. * A minimal helix is defined by two consecutive n-turns.
  615. * For example, a 4-helix, of minimal length 4 from residues i to i + 3,
  616. * requires 4-turns at residues i - 1 and i,
  617. *
  618. * 3-helix(i,i + 2)=: [3-turn(i - 1) and 3-turn(i)]
  619. * 4-helix(i,i + 3)=: [4-turn(i - 1) and 4-turn(i)]
  620. * 5-helix(i,i + 4)=: [5-turn(i - 1) and 5-turn(i)]
  621. *
  622. * Type: G (n=3), H (n=4), I (n=5)
  623. */
  624. function assignHelices(ctx: DSSPContext) {
  625. const { proteinResidues, flags } = ctx
  626. const turnFlag = [DSSPType.Flag.T3S, DSSPType.Flag.T4S, DSSPType.Flag.T5S, DSSPType.Flag.T3, DSSPType.Flag.T4, DSSPType.Flag.T5]
  627. const helixFlag = [0, 0, 0, DSSPType.Flag.G, DSSPType.Flag.H, DSSPType.Flag.I]
  628. const helixCheckOrder = ctx.oldOrdering ? [4, 3, 5] : [3, 4, 5]
  629. // const count = [0, 0, 0, 0, 0, 0]
  630. for (let ni = 0; ni < helixCheckOrder.length; ni++) {
  631. const n = helixCheckOrder[ni]
  632. for (let i = 1, il = proteinResidues.length - n; i < il; i++) {
  633. const fI = DSSPType.create(flags[i])
  634. const fI1 = DSSPType.create(flags[i - 1])
  635. const fI2 = DSSPType.create(flags[i + 1])
  636. // TODO rework to elegant solution which will not break instantly
  637. if (ctx.oldOrdering) {
  638. if ((n === 3 && (DSSPType.is(fI, DSSPType.Flag.H) || DSSPType.is(fI2, DSSPType.Flag.H)) || // for 3-10 yield to alpha helix
  639. (n === 5 && ((DSSPType.is(fI, DSSPType.Flag.H) || DSSPType.is(fI, DSSPType.Flag.G)) || (DSSPType.is(fI2, DSSPType.Flag.H) || DSSPType.is(fI2, DSSPType.Flag.G)))))) { // for pi yield to all other helices
  640. // console.log('skipping in favor of more preferable helix already present')
  641. continue
  642. }
  643. } else {
  644. if ((n === 4 && (DSSPType.is(fI, DSSPType.Flag.G) || DSSPType.is(fI2, DSSPType.Flag.G)) || // for alpha helix yield to 3-10
  645. (n === 5 && ((DSSPType.is(fI, DSSPType.Flag.H) || DSSPType.is(fI, DSSPType.Flag.G)) || (DSSPType.is(fI2, DSSPType.Flag.H) || DSSPType.is(fI2, DSSPType.Flag.G)))))) { // for pi yield to all other helices
  646. // console.log('skipping in favor of more preferable helix already present')
  647. continue
  648. }
  649. }
  650. if (DSSPType.is(fI, turnFlag[n]) && DSSPType.is(fI, turnFlag[n - 3]) && // check fI for turn start of proper type
  651. DSSPType.is(fI1, turnFlag[n]) && DSSPType.is(fI1, turnFlag[n - 3])) { // check fI1 accordingly
  652. // console.log(`valid hit for ${n} from ${i} to ${i+n}`)
  653. if (ctx.oldDefinition) {
  654. for (let k = 0; k < n; k++) {
  655. flags[i + k] |= helixFlag[n]
  656. // count[n]++
  657. }
  658. } else {
  659. for (let k = -1; k <= n; k++) {
  660. flags[i + k] |= helixFlag[n]
  661. // count[n]++
  662. }
  663. }
  664. }
  665. }
  666. }
  667. // console.log(`${ count[3] + count[4] + count[5] } helix elements - ${ count[3] } 3-10, ${ count[4] } alpha, ${ count[5] } pi`)
  668. }
  669. function angle(a: Vec3, b: Vec3, c: Vec3, d: Vec3) {
  670. const ab = Vec3.zero();
  671. const cb = Vec3.zero();
  672. const bc = Vec3.zero();
  673. const dc = Vec3.zero();
  674. Vec3.sub(ab, a, b);
  675. Vec3.sub(cb, c, b);
  676. Vec3.sub(bc, b, c);
  677. Vec3.sub(dc, d, c);
  678. const abc = Vec3.zero();
  679. const bcd = Vec3.zero();
  680. Vec3.cross(abc, ab, cb);
  681. Vec3.cross(bcd, bc, dc);
  682. const angle = Vec3.angle(abc, bcd) * 360.0 / (2 * Math.PI);
  683. const cross = Vec3.zero();
  684. Vec3.cross(cross, abc, bcd);
  685. const value = Vec3.dot(cb, cross);
  686. return value > 0 ? angle : -angle;
  687. }
  688. /**
  689. * ladder=: set of one or more consecutive bridges of identical type
  690. *
  691. * Type: E
  692. */
  693. function assignLadders(ctx: DSSPContext) {
  694. const { bridges, ladders } = ctx
  695. // let ext = 0
  696. // create ladders
  697. for (let bridgeIndex = 0; bridgeIndex < bridges.length; bridgeIndex++) {
  698. const bridge = bridges[bridgeIndex]
  699. let found = false
  700. for (let ladderIndex = 0; ladderIndex < ladders.length; ladderIndex++) {
  701. const ladder = ladders[ladderIndex]
  702. if (shouldExtendLadder(ladder, bridge)) {
  703. // ext++
  704. found = true
  705. ladder.firstEnd++
  706. if (bridge.type === BridgeType.PARALLEL) {
  707. ladder.secondEnd++
  708. } else {
  709. ladder.secondStart--
  710. }
  711. }
  712. }
  713. // no suitable assignment: create new ladder with single bridge as content
  714. if (!found) {
  715. ladders[ladders.length] = {
  716. previousLadder: 0,
  717. nextLadder: 0,
  718. firstStart: bridge.partner1,
  719. firstEnd: bridge.partner1,
  720. secondStart: bridge.partner2,
  721. secondEnd: bridge.partner2,
  722. type: bridge.type
  723. }
  724. }
  725. }
  726. // console.log(`${ ext } extension operations`)
  727. // connect ladders
  728. for (let ladderIndex1 = 0; ladderIndex1 < ladders.length; ladderIndex1++) {
  729. const ladder1 = ladders[ladderIndex1]
  730. for (let ladderIndex2 = ladderIndex1; ladderIndex2 < ladders.length; ladderIndex2++) {
  731. const ladder2 = ladders[ladderIndex2]
  732. if (resemblesBulge(ladder1, ladder2)) {
  733. // console.log(`connecting ladders ${ ladderIndex1 } and ${ ladderIndex2 } with inbetween bulge`)
  734. ladder1.nextLadder = ladderIndex2
  735. ladder2.previousLadder = ladderIndex1
  736. }
  737. }
  738. }
  739. // console.log(`${ ladders.length } ladders`)
  740. }
  741. /**
  742. * For beta structures, we define: a bulge-linked ladder consists of two ladders or bridges of the same type
  743. * connected by at most one extra residue of one strand and at most four extra residues on the other strand,
  744. * all residues in bulge-linked ladders are marked E, including any extra residues.
  745. */
  746. function resemblesBulge(ladder1: Ladder, ladder2: Ladder) {
  747. if (!(ladder1.type === ladder2.type && ladder2.firstStart - ladder1.firstEnd < 6 &&
  748. ladder1.firstStart < ladder2.firstStart && ladder2.nextLadder === 0)) return false
  749. if (ladder1.type === BridgeType.PARALLEL) {
  750. return bulgeCriterion2(ladder1, ladder2)
  751. } else {
  752. return bulgeCriterion2(ladder2, ladder1)
  753. }
  754. }
  755. function bulgeCriterion2(ladder1: Ladder, ladder2: Ladder) {
  756. return ladder2.secondStart - ladder1.secondEnd > 0 && ((ladder2.secondStart - ladder1.secondEnd < 6 &&
  757. ladder2.firstStart - ladder1.firstEnd < 3) || ladder2.secondStart - ladder1.secondEnd < 3)
  758. }
  759. function shouldExtendLadder(ladder: Ladder, bridge: Bridge): boolean {
  760. // in order to extend ladders, same type must be present
  761. if (bridge.type !== ladder.type) return false
  762. // console.log(`${ bridge.partner1 }-${ bridge.partner2 } ${ ladder.firstEnd }`)
  763. // only extend if residue 1 is sequence neighbor with regard to ladder
  764. if (bridge.partner1 !== ladder.firstEnd + 1) return false
  765. if (bridge.type === BridgeType.PARALLEL) {
  766. if (bridge.partner2 === ladder.secondEnd + 1) {
  767. // console.log(ladder)
  768. return true
  769. }
  770. } else {
  771. if (bridge.partner2 === ladder.secondStart - 1) {
  772. // console.log(ladder)
  773. return true
  774. }
  775. }
  776. return false
  777. }
  778. function isHelixType(f: DSSPType) {
  779. return DSSPType.is(f, DSSPType.Flag.G) || DSSPType.is(f, DSSPType.Flag.H) || DSSPType.is(f, DSSPType.Flag.I)
  780. }
  781. /**
  782. * sheet=: set of one or more ladders connected by shared residues
  783. *
  784. * Type: E
  785. */
  786. function assignSheets(ctx: DSSPContext) {
  787. const { ladders, flags } = ctx
  788. for (let ladderIndex = 0; ladderIndex < ladders.length; ladderIndex++) {
  789. const ladder = ladders[ladderIndex]
  790. for (let lcount = ladder.firstStart; lcount <= ladder.firstEnd; lcount++) {
  791. const diff = ladder.firstStart - lcount
  792. const l2count = ladder.secondStart - diff
  793. if (ladder.firstStart !== ladder.firstEnd) {
  794. flags[lcount] |= DSSPType.Flag.E
  795. flags[l2count] |= DSSPType.Flag.E
  796. } else {
  797. if (!isHelixType(flags[lcount]) && DSSPType.is(flags[lcount], DSSPType.Flag.E)) {
  798. flags[lcount] |= DSSPType.Flag.B
  799. }
  800. if (!isHelixType(flags[l2count]) && DSSPType.is(flags[l2count], DSSPType.Flag.E)) {
  801. flags[l2count] |= DSSPType.Flag.B
  802. }
  803. }
  804. }
  805. if (ladder.nextLadder === 0) continue
  806. const conladder = ladders[ladder.nextLadder]
  807. for (let lcount = ladder.firstStart; lcount <= conladder.firstEnd; lcount++) {
  808. flags[lcount] |= DSSPType.Flag.E
  809. }
  810. if (ladder.type === BridgeType.PARALLEL) {
  811. for (let lcount = ladder.secondStart; lcount <= conladder.secondEnd; lcount++) {
  812. flags[lcount] |= DSSPType.Flag.E
  813. }
  814. } else {
  815. for (let lcount = conladder.secondEnd; lcount <= ladder.secondStart; lcount++) {
  816. flags[lcount] |= DSSPType.Flag.E
  817. }
  818. }
  819. }
  820. }