molecular-surface.ts 14 KB

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