molecular-surface.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. /**
  2. * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author Fred Ludlow <fred.ludlow@gmail.com>
  5. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  6. *
  7. * ported from NGL (https://github.com/arose/ngl), licensed under MIT
  8. */
  9. import { Vec3, Tensor } from '../../mol-math/linear-algebra';
  10. import { ParamDefinition as PD } from '../../mol-util/param-definition';
  11. import { RuntimeContext } from '../../mol-task';
  12. import { OrderedSet } from '../../mol-data/int';
  13. import { PositionData } from './common';
  14. import { Mat4 } from '../../mol-math/linear-algebra/3d';
  15. import { Box3D, GridLookup3D, fillGridDim } from '../../mol-math/geometry';
  16. function normalToLine (out: Vec3, p: Vec3) {
  17. out[0] = out[1] = out[2] = 1.0
  18. if (p[0] !== 0) {
  19. out[0] = (p[1] + p[2]) / -p[0]
  20. } else if (p[1] !== 0) {
  21. out[1] = (p[0] + p[2]) / -p[1]
  22. } else if (p[2] !== 0) {
  23. out[2] = (p[0] + p[1]) / -p[2]
  24. }
  25. return out
  26. }
  27. type AnglesTables = { cosTable: Float32Array, sinTable: Float32Array }
  28. function getAngleTables (probePositions: number): AnglesTables {
  29. let theta = 0.0
  30. const step = 2 * Math.PI / probePositions
  31. const cosTable = new Float32Array(probePositions)
  32. const sinTable = new Float32Array(probePositions)
  33. for (let i = 0; i < probePositions; i++) {
  34. cosTable[i] = Math.cos(theta)
  35. sinTable[i] = Math.sin(theta)
  36. theta += step
  37. }
  38. return { cosTable, sinTable}
  39. }
  40. //
  41. export const MolecularSurfaceCalculationParams = {
  42. resolution: PD.Numeric(0.5, { min: 0.01, max: 20, step: 0.01 }),
  43. probeRadius: PD.Numeric(1.4, { min: 0, max: 10, step: 0.1 }),
  44. probePositions: PD.Numeric(30, { min: 12, max: 90, step: 1 }),
  45. }
  46. export const DefaultMolecularSurfaceCalculationProps = PD.getDefaultValues(MolecularSurfaceCalculationParams)
  47. export type MolecularSurfaceCalculationProps = typeof DefaultMolecularSurfaceCalculationProps
  48. export async function calcMolecularSurface(ctx: RuntimeContext, position: Required<PositionData>, maxRadius: number, box: Box3D | null, props: MolecularSurfaceCalculationProps) {
  49. // Field generation method adapted from AstexViewer (Mike Hartshorn) by Fred Ludlow.
  50. // Other parts based heavily on NGL (Alexander Rose) EDT Surface class
  51. let lastClip = -1
  52. /**
  53. * Is the point at x,y,z obscured by any of the atoms specifeid by indices in neighbours.
  54. * Ignore indices a and b (these are the relevant atoms in projectPoints/Torii)
  55. *
  56. * Cache the last clipped atom (as very often the same one in subsequent calls)
  57. *
  58. * `a` and `b` must be resolved indices
  59. */
  60. function obscured(x: number, y: number, z: number, a: number, b: number) {
  61. if (lastClip !== -1) {
  62. const ai = lastClip
  63. if (ai !== a && ai !== b && singleAtomObscures(ai, x, y, z)) {
  64. return ai
  65. } else {
  66. lastClip = -1
  67. }
  68. }
  69. for (let j = 0, jl = neighbours.count; j < jl; ++j) {
  70. const ai = OrderedSet.getAt(indices, neighbours.indices[j])
  71. if (ai !== a && ai !== b && singleAtomObscures(ai, x, y, z)) {
  72. lastClip = ai
  73. return ai
  74. }
  75. }
  76. return -1
  77. }
  78. /**
  79. * `ai` must be a resolved index
  80. */
  81. function singleAtomObscures(ai: number, x: number, y: number, z: number) {
  82. const r = radius[ai]
  83. const dx = px[ai] - x
  84. const dy = py[ai] - y
  85. const dz = pz[ai] - z
  86. const dSq = dx * dx + dy * dy + dz * dz
  87. return dSq < (r * r)
  88. }
  89. /**
  90. * For each atom:
  91. * Iterate over a subsection of the grid, for each point:
  92. * If current value < 0.0, unvisited, set positive
  93. *
  94. * In any case: Project this point onto surface of the atomic sphere
  95. * If this projected point is not obscured by any other atom
  96. * Calculate delta distance and set grid value to minimum of
  97. * itself and delta
  98. */
  99. function projectPointsRange (begI: number, endI: number) {
  100. for (let i = begI; i < endI; ++i) {
  101. const j = OrderedSet.getAt(indices, i)
  102. const vx = px[j], vy = py[j], vz = pz[j]
  103. const rad = radius[j]
  104. const rSq = rad * rad
  105. lookup3d.find(vx, vy, vz, rad)
  106. // Number of grid points, round this up...
  107. const ng = Math.ceil(rad * scaleFactor)
  108. // Center of the atom, mapped to grid points (take floor)
  109. const iax = Math.floor(scaleFactor * (vx - minX))
  110. const iay = Math.floor(scaleFactor * (vy - minY))
  111. const iaz = Math.floor(scaleFactor * (vz - minZ))
  112. // Extents of grid to consider for this atom
  113. const begX = Math.max(0, iax - ng)
  114. const begY = Math.max(0, iay - ng)
  115. const begZ = Math.max(0, iaz - ng)
  116. // Add two to these points:
  117. // - iax are floor'd values so this ensures coverage
  118. // - these are loop limits (exclusive)
  119. const endX = Math.min(dimX, iax + ng + 2)
  120. const endY = Math.min(dimY, iay + ng + 2)
  121. const endZ = Math.min(dimZ, iaz + ng + 2)
  122. for (let xi = begX; xi < endX; ++xi) {
  123. const dx = gridx[xi] - vx
  124. const xIdx = xi * iuv
  125. for (let yi = begY; yi < endY; ++yi) {
  126. const dy = gridy[yi] - vy
  127. const dxySq = dx * dx + dy * dy
  128. const xyIdx = yi * iu + xIdx
  129. for (let zi = begZ; zi < endZ; ++zi) {
  130. const dz = gridz[zi] - vz
  131. const dSq = dxySq + dz * dz
  132. if (dSq < rSq) {
  133. const idx = zi + xyIdx
  134. // if unvisited, make positive
  135. if (data[idx] < 0.0) data[idx] *= -1
  136. // Project on to the surface of the sphere
  137. // sp is the projected point ( dx, dy, dz ) * ( ra / d )
  138. const d = Math.sqrt(dSq)
  139. const ap = rad / d
  140. const spx = dx * ap + vx
  141. const spy = dy * ap + vy
  142. const spz = dz * ap + vz
  143. if (obscured(spx, spy, spz, j, -1) === -1) {
  144. const dd = rad - d
  145. if (dd < data[idx]) {
  146. data[idx] = dd
  147. idData[idx] = id[i]
  148. }
  149. }
  150. }
  151. }
  152. }
  153. }
  154. }
  155. }
  156. async function projectPoints() {
  157. for (let i = 0; i < n; i += updateChunk) {
  158. projectPointsRange(i, Math.min(i + updateChunk, n))
  159. if (ctx.shouldUpdate) {
  160. await ctx.update({ message: 'projecting points', current: i, max: n })
  161. }
  162. }
  163. }
  164. // Vectors for Torus Projection
  165. const atob = Vec3()
  166. const mid = Vec3()
  167. const n1 = Vec3()
  168. const n2 = Vec3()
  169. /**
  170. * `a` and `b` must be resolved indices
  171. */
  172. function projectTorus(a: number, b: number) {
  173. const rA = radius[a]
  174. const rB = radius[b]
  175. const dx = atob[0] = px[b] - px[a]
  176. const dy = atob[1] = py[b] - py[a]
  177. const dz = atob[2] = pz[b] - pz[a]
  178. const dSq = dx * dx + dy * dy + dz * dz
  179. // This check now redundant as already done in AVHash.withinRadii
  180. // if (dSq > ((rA + rB) * (rA + rB))) { return }
  181. const d = Math.sqrt(dSq)
  182. // Find angle between a->b vector and the circle
  183. // of their intersection by cosine rule
  184. const cosA = (rA * rA + d * d - rB * rB) / (2.0 * rA * d)
  185. // distance along a->b at intersection
  186. const dmp = rA * cosA
  187. Vec3.normalize(atob, atob)
  188. // Create normal to line
  189. normalToLine(n1, atob)
  190. Vec3.normalize(n1, n1)
  191. // Cross together for second normal vector
  192. Vec3.cross(n2, atob, n1)
  193. Vec3.normalize(n2, n2)
  194. // r is radius of circle of intersection
  195. const rInt = Math.sqrt(rA * rA - dmp * dmp)
  196. Vec3.scale(n1, n1, rInt)
  197. Vec3.scale(n2, n2, rInt)
  198. Vec3.scale(atob, atob, dmp)
  199. mid[0] = atob[0] + px[a]
  200. mid[1] = atob[1] + py[a]
  201. mid[2] = atob[2] + pz[a]
  202. lastClip = -1
  203. for (let i = 0; i < probePositions; ++i) {
  204. const cost = cosTable[i]
  205. const sint = sinTable[i]
  206. const px = mid[0] + cost * n1[0] + sint * n2[0]
  207. const py = mid[1] + cost * n1[1] + sint * n2[1]
  208. const pz = mid[2] + cost * n1[2] + sint * n2[2]
  209. if (obscured(px, py, pz, a, b) === -1) {
  210. const iax = Math.floor(scaleFactor * (px - minX))
  211. const iay = Math.floor(scaleFactor * (py - minY))
  212. const iaz = Math.floor(scaleFactor * (pz - minZ))
  213. const begX = Math.max(0, iax - ngTorus)
  214. const begY = Math.max(0, iay - ngTorus)
  215. const begZ = Math.max(0, iaz - ngTorus)
  216. const endX = Math.min(dimX, iax + ngTorus + 2)
  217. const endY = Math.min(dimY, iay + ngTorus + 2)
  218. const endZ = Math.min(dimZ, iaz + ngTorus + 2)
  219. for (let xi = begX; xi < endX; ++xi) {
  220. const dx = px - gridx[xi]
  221. const xIdx = xi * iuv
  222. for (let yi = begY; yi < endY; ++yi) {
  223. const dy = py - gridy[yi]
  224. const dxySq = dx * dx + dy * dy
  225. const xyIdx = yi * iu + xIdx
  226. for (let zi = begZ; zi < endZ; ++zi) {
  227. const dz = pz - gridz[zi]
  228. const dSq = dxySq + dz * dz
  229. const idx = zi + xyIdx
  230. const current = data[idx]
  231. if (current > 0.0 && dSq < (current * current)) {
  232. data[idx] = Math.sqrt(dSq)
  233. // Is this grid point closer to a or b?
  234. // Take dot product of atob and gridpoint->p (dx, dy, dz)
  235. const dp = dx * atob[0] + dy * atob[1] + dz * atob[2]
  236. idData[idx] = id[OrderedSet.indexOf(indices, dp < 0.0 ? b : a)]
  237. }
  238. }
  239. }
  240. }
  241. }
  242. }
  243. }
  244. function projectToriiRange (begI: number, endI: number) {
  245. for (let i = begI; i < endI; ++i) {
  246. const k = OrderedSet.getAt(indices, i)
  247. lookup3d.find(px[k], py[k], pz[k], radius[k])
  248. for (let j = 0, jl = neighbours.count; j < jl; ++j) {
  249. const l = OrderedSet.getAt(indices, neighbours.indices[j])
  250. if (k < l) projectTorus(k, l)
  251. }
  252. }
  253. }
  254. async function projectTorii() {
  255. for (let i = 0; i < n; i += updateChunk) {
  256. projectToriiRange(i, Math.min(i + updateChunk, n))
  257. if (ctx.shouldUpdate) {
  258. await ctx.update({ message: 'projecting torii', current: i, max: n })
  259. }
  260. }
  261. }
  262. // console.time('MolecularSurface')
  263. // console.time('MolecularSurface createState')
  264. const { resolution, probeRadius, probePositions } = props
  265. const scaleFactor = 1 / resolution
  266. const ngTorus = Math.max(5, 2 + Math.floor(probeRadius * scaleFactor))
  267. const cellSize = Vec3.create(maxRadius, maxRadius, maxRadius)
  268. Vec3.scale(cellSize, cellSize, 2)
  269. const lookup3d = GridLookup3D(position, cellSize)
  270. const neighbours = lookup3d.result
  271. if (box === null) box = lookup3d.boundary.box
  272. const { indices, x: px, y: py, z: pz, id, radius } = position
  273. const n = OrderedSet.size(indices)
  274. const pad = maxRadius + resolution
  275. const expandedBox = Box3D.expand(Box3D(), box, Vec3.create(pad, pad, pad));
  276. const [ minX, minY, minZ ] = expandedBox.min
  277. const scaledBox = Box3D.scale(Box3D(), expandedBox, scaleFactor)
  278. const dim = Box3D.size(Vec3(), scaledBox)
  279. Vec3.ceil(dim, dim)
  280. const [ dimX, dimY, dimZ ] = dim
  281. const iu = dimZ, iv = dimY, iuv = iu * iv
  282. const { cosTable, sinTable } = getAngleTables(probePositions)
  283. const space = Tensor.Space(dim, [0, 1, 2], Float32Array)
  284. const data = space.create()
  285. const idData = space.create()
  286. data.fill(-1001.0)
  287. idData.fill(-1)
  288. const gridx = fillGridDim(dimX, minX, resolution)
  289. const gridy = fillGridDim(dimY, minY, resolution)
  290. const gridz = fillGridDim(dimZ, minZ, resolution)
  291. const updateChunk = Math.ceil(100000 / ((Math.pow(Math.pow(maxRadius, 3), 3) * scaleFactor)))
  292. // console.timeEnd('MolecularSurface createState')
  293. // console.time('MolecularSurface projectPoints')
  294. await projectPoints()
  295. // console.timeEnd('MolecularSurface projectPoints')
  296. // console.time('MolecularSurface projectTorii')
  297. await projectTorii()
  298. // console.timeEnd('MolecularSurface projectTorii')
  299. // console.timeEnd('MolecularSurface')
  300. const field = Tensor.create(space, data)
  301. const idField = Tensor.create(space, idData)
  302. const transform = Mat4.identity()
  303. Mat4.fromScaling(transform, Vec3.create(resolution, resolution, resolution))
  304. Mat4.setTranslation(transform, expandedBox.min)
  305. // console.log({ field, idField, transform, updateChunk })
  306. return { field, idField, transform }
  307. }