123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372 |
- /**
- * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Fred Ludlow <fred.ludlow@gmail.com>
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- *
- * ported from NGL (https://github.com/arose/ngl), licensed under MIT
- */
- import { Vec3, Tensor } from '../../mol-math/linear-algebra';
- import { ParamDefinition as PD } from '../../mol-util/param-definition';
- import { RuntimeContext } from '../../mol-task';
- import { OrderedSet } from '../../mol-data/int';
- import { PositionData } from './common';
- import { Mat4 } from '../../mol-math/linear-algebra/3d';
- import { Box3D, GridLookup3D, fillGridDim } from '../../mol-math/geometry';
- function normalToLine (out: Vec3, p: Vec3) {
- out[0] = out[1] = out[2] = 1.0
- if (p[0] !== 0) {
- out[0] = (p[1] + p[2]) / -p[0]
- } else if (p[1] !== 0) {
- out[1] = (p[0] + p[2]) / -p[1]
- } else if (p[2] !== 0) {
- out[2] = (p[0] + p[1]) / -p[2]
- }
- return out
- }
- type AnglesTables = { cosTable: Float32Array, sinTable: Float32Array }
- function getAngleTables (probePositions: number): AnglesTables {
- let theta = 0.0
- const step = 2 * Math.PI / probePositions
- const cosTable = new Float32Array(probePositions)
- const sinTable = new Float32Array(probePositions)
- for (let i = 0; i < probePositions; i++) {
- cosTable[i] = Math.cos(theta)
- sinTable[i] = Math.sin(theta)
- theta += step
- }
- return { cosTable, sinTable}
- }
- //
- export const MolecularSurfaceCalculationParams = {
- resolution: PD.Numeric(0.5, { min: 0.01, max: 20, step: 0.01 }),
- probeRadius: PD.Numeric(1.4, { min: 0, max: 10, step: 0.1 }),
- probePositions: PD.Numeric(30, { min: 12, max: 90, step: 1 }),
- }
- export const DefaultMolecularSurfaceCalculationProps = PD.getDefaultValues(MolecularSurfaceCalculationParams)
- export type MolecularSurfaceCalculationProps = typeof DefaultMolecularSurfaceCalculationProps
- export async function calcMolecularSurface(ctx: RuntimeContext, position: Required<PositionData>, maxRadius: number, box: Box3D | null, props: MolecularSurfaceCalculationProps) {
- // Field generation method adapted from AstexViewer (Mike Hartshorn) by Fred Ludlow.
- // Other parts based heavily on NGL (Alexander Rose) EDT Surface class
- let lastClip = -1
- /**
- * Is the point at x,y,z obscured by any of the atoms specifeid by indices in neighbours.
- * Ignore indices a and b (these are the relevant atoms in projectPoints/Torii)
- *
- * Cache the last clipped atom (as very often the same one in subsequent calls)
- *
- * `a` and `b` must be resolved indices
- */
- function obscured(x: number, y: number, z: number, a: number, b: number) {
- if (lastClip !== -1) {
- const ai = lastClip
- if (ai !== a && ai !== b && singleAtomObscures(ai, x, y, z)) {
- return ai
- } else {
- lastClip = -1
- }
- }
- for (let j = 0, jl = neighbours.count; j < jl; ++j) {
- const ai = OrderedSet.getAt(indices, neighbours.indices[j])
- if (ai !== a && ai !== b && singleAtomObscures(ai, x, y, z)) {
- lastClip = ai
- return ai
- }
- }
- return -1
- }
- /**
- * `ai` must be a resolved index
- */
- function singleAtomObscures(ai: number, x: number, y: number, z: number) {
- const r = radius[ai]
- const dx = px[ai] - x
- const dy = py[ai] - y
- const dz = pz[ai] - z
- const dSq = dx * dx + dy * dy + dz * dz
- return dSq < (r * r)
- }
- /**
- * For each atom:
- * Iterate over a subsection of the grid, for each point:
- * If current value < 0.0, unvisited, set positive
- *
- * In any case: Project this point onto surface of the atomic sphere
- * If this projected point is not obscured by any other atom
- * Calculate delta distance and set grid value to minimum of
- * itself and delta
- */
- function projectPointsRange (begI: number, endI: number) {
- for (let i = begI; i < endI; ++i) {
- const j = OrderedSet.getAt(indices, i)
- const vx = px[j], vy = py[j], vz = pz[j]
- const rad = radius[j]
- const rSq = rad * rad
- lookup3d.find(vx, vy, vz, rad)
- // Number of grid points, round this up...
- const ng = Math.ceil(rad * scaleFactor)
- // Center of the atom, mapped to grid points (take floor)
- const iax = Math.floor(scaleFactor * (vx - minX))
- const iay = Math.floor(scaleFactor * (vy - minY))
- const iaz = Math.floor(scaleFactor * (vz - minZ))
- // Extents of grid to consider for this atom
- const begX = Math.max(0, iax - ng)
- const begY = Math.max(0, iay - ng)
- const begZ = Math.max(0, iaz - ng)
- // Add two to these points:
- // - iax are floor'd values so this ensures coverage
- // - these are loop limits (exclusive)
- const endX = Math.min(dimX, iax + ng + 2)
- const endY = Math.min(dimY, iay + ng + 2)
- const endZ = Math.min(dimZ, iaz + ng + 2)
- for (let xi = begX; xi < endX; ++xi) {
- const dx = gridx[xi] - vx
- const xIdx = xi * iuv
- for (let yi = begY; yi < endY; ++yi) {
- const dy = gridy[yi] - vy
- const dxySq = dx * dx + dy * dy
- const xyIdx = yi * iu + xIdx
- for (let zi = begZ; zi < endZ; ++zi) {
- const dz = gridz[zi] - vz
- const dSq = dxySq + dz * dz
- if (dSq < rSq) {
- const idx = zi + xyIdx
- // if unvisited, make positive
- if (data[idx] < 0.0) data[idx] *= -1
- // Project on to the surface of the sphere
- // sp is the projected point ( dx, dy, dz ) * ( ra / d )
- const d = Math.sqrt(dSq)
- const ap = rad / d
- const spx = dx * ap + vx
- const spy = dy * ap + vy
- const spz = dz * ap + vz
- if (obscured(spx, spy, spz, j, -1) === -1) {
- const dd = rad - d
- if (dd < data[idx]) {
- data[idx] = dd
- idData[idx] = id[i]
- }
- }
- }
- }
- }
- }
- }
- }
- async function projectPoints() {
- for (let i = 0; i < n; i += updateChunk) {
- projectPointsRange(i, Math.min(i + updateChunk, n))
- if (ctx.shouldUpdate) {
- await ctx.update({ message: 'projecting points', current: i, max: n })
- }
- }
- }
- // Vectors for Torus Projection
- const atob = Vec3()
- const mid = Vec3()
- const n1 = Vec3()
- const n2 = Vec3()
- /**
- * `a` and `b` must be resolved indices
- */
- function projectTorus(a: number, b: number) {
- const rA = radius[a]
- const rB = radius[b]
- const dx = atob[0] = px[b] - px[a]
- const dy = atob[1] = py[b] - py[a]
- const dz = atob[2] = pz[b] - pz[a]
- const dSq = dx * dx + dy * dy + dz * dz
- // This check now redundant as already done in AVHash.withinRadii
- // if (dSq > ((rA + rB) * (rA + rB))) { return }
- const d = Math.sqrt(dSq)
- // Find angle between a->b vector and the circle
- // of their intersection by cosine rule
- const cosA = (rA * rA + d * d - rB * rB) / (2.0 * rA * d)
- // distance along a->b at intersection
- const dmp = rA * cosA
- Vec3.normalize(atob, atob)
- // Create normal to line
- normalToLine(n1, atob)
- Vec3.normalize(n1, n1)
- // Cross together for second normal vector
- Vec3.cross(n2, atob, n1)
- Vec3.normalize(n2, n2)
- // r is radius of circle of intersection
- const rInt = Math.sqrt(rA * rA - dmp * dmp)
- Vec3.scale(n1, n1, rInt)
- Vec3.scale(n2, n2, rInt)
- Vec3.scale(atob, atob, dmp)
- mid[0] = atob[0] + px[a]
- mid[1] = atob[1] + py[a]
- mid[2] = atob[2] + pz[a]
- lastClip = -1
- for (let i = 0; i < probePositions; ++i) {
- const cost = cosTable[i]
- const sint = sinTable[i]
- const px = mid[0] + cost * n1[0] + sint * n2[0]
- const py = mid[1] + cost * n1[1] + sint * n2[1]
- const pz = mid[2] + cost * n1[2] + sint * n2[2]
- if (obscured(px, py, pz, a, b) === -1) {
- const iax = Math.floor(scaleFactor * (px - minX))
- const iay = Math.floor(scaleFactor * (py - minY))
- const iaz = Math.floor(scaleFactor * (pz - minZ))
- const begX = Math.max(0, iax - ngTorus)
- const begY = Math.max(0, iay - ngTorus)
- const begZ = Math.max(0, iaz - ngTorus)
- const endX = Math.min(dimX, iax + ngTorus + 2)
- const endY = Math.min(dimY, iay + ngTorus + 2)
- const endZ = Math.min(dimZ, iaz + ngTorus + 2)
- for (let xi = begX; xi < endX; ++xi) {
- const dx = px - gridx[xi]
- const xIdx = xi * iuv
- for (let yi = begY; yi < endY; ++yi) {
- const dy = py - gridy[yi]
- const dxySq = dx * dx + dy * dy
- const xyIdx = yi * iu + xIdx
- for (let zi = begZ; zi < endZ; ++zi) {
- const dz = pz - gridz[zi]
- const dSq = dxySq + dz * dz
- const idx = zi + xyIdx
- const current = data[idx]
- if (current > 0.0 && dSq < (current * current)) {
- data[idx] = Math.sqrt(dSq)
- // Is this grid point closer to a or b?
- // Take dot product of atob and gridpoint->p (dx, dy, dz)
- const dp = dx * atob[0] + dy * atob[1] + dz * atob[2]
- idData[idx] = id[OrderedSet.indexOf(indices, dp < 0.0 ? b : a)]
- }
- }
- }
- }
- }
- }
- }
- function projectToriiRange (begI: number, endI: number) {
- for (let i = begI; i < endI; ++i) {
- const k = OrderedSet.getAt(indices, i)
- lookup3d.find(px[k], py[k], pz[k], radius[k])
- for (let j = 0, jl = neighbours.count; j < jl; ++j) {
- const l = OrderedSet.getAt(indices, neighbours.indices[j])
- if (k < l) projectTorus(k, l)
- }
- }
- }
- async function projectTorii() {
- for (let i = 0; i < n; i += updateChunk) {
- projectToriiRange(i, Math.min(i + updateChunk, n))
- if (ctx.shouldUpdate) {
- await ctx.update({ message: 'projecting torii', current: i, max: n })
- }
- }
- }
- // console.time('MolecularSurface')
- // console.time('MolecularSurface createState')
- const { resolution, probeRadius, probePositions } = props
- const scaleFactor = 1 / resolution
- const ngTorus = Math.max(5, 2 + Math.floor(probeRadius * scaleFactor))
- const cellSize = Vec3.create(maxRadius, maxRadius, maxRadius)
- Vec3.scale(cellSize, cellSize, 2)
- const lookup3d = GridLookup3D(position, cellSize)
- const neighbours = lookup3d.result
- if (box === null) box = lookup3d.boundary.box
- const { indices, x: px, y: py, z: pz, id, radius } = position
- const n = OrderedSet.size(indices)
- const pad = maxRadius + resolution
- const expandedBox = Box3D.expand(Box3D(), box, Vec3.create(pad, pad, pad));
- const [ minX, minY, minZ ] = expandedBox.min
- const scaledBox = Box3D.scale(Box3D(), expandedBox, scaleFactor)
- const dim = Box3D.size(Vec3(), scaledBox)
- Vec3.ceil(dim, dim)
- const [ dimX, dimY, dimZ ] = dim
- const iu = dimZ, iv = dimY, iuv = iu * iv
- const { cosTable, sinTable } = getAngleTables(probePositions)
- const space = Tensor.Space(dim, [0, 1, 2], Float32Array)
- const data = space.create()
- const idData = space.create()
- data.fill(-1001.0)
- idData.fill(-1)
- const gridx = fillGridDim(dimX, minX, resolution)
- const gridy = fillGridDim(dimY, minY, resolution)
- const gridz = fillGridDim(dimZ, minZ, resolution)
- const updateChunk = Math.ceil(100000 / ((Math.pow(Math.pow(maxRadius, 3), 3) * scaleFactor)))
- // console.timeEnd('MolecularSurface createState')
- // console.time('MolecularSurface projectPoints')
- await projectPoints()
- // console.timeEnd('MolecularSurface projectPoints')
- // console.time('MolecularSurface projectTorii')
- await projectTorii()
- // console.timeEnd('MolecularSurface projectTorii')
- // console.timeEnd('MolecularSurface')
- const field = Tensor.create(space, data)
- const idField = Tensor.create(space, idData)
- const transform = Mat4.identity()
- Mat4.fromScaling(transform, Vec3.create(resolution, resolution, resolution))
- Mat4.setTranslation(transform, expandedBox.min)
- // console.log({ field, idField, transform, updateChunk })
- return { field, idField, transform }
- }
|