|
@@ -10,7 +10,6 @@
|
|
|
import { fillUniform } from 'mol-util/array';
|
|
|
import { Vec3, Tensor } from 'mol-math/linear-algebra';
|
|
|
import { ParamDefinition as PD } from 'mol-util/param-definition';
|
|
|
-import { Lookup3D, Result } from './lookup3d/common';
|
|
|
import { RuntimeContext } from 'mol-task';
|
|
|
import { OrderedSet } from 'mol-data/int';
|
|
|
import { PositionData } from './common';
|
|
@@ -44,320 +43,293 @@ function getAngleTables (probePositions: number): AnglesTables {
|
|
|
return { cosTable, sinTable}
|
|
|
}
|
|
|
|
|
|
-/**
|
|
|
- * 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 (state: MolSurfCalcState, x: number, y: number, z: number, a: number, b: number) {
|
|
|
- if (state.lastClip !== -1) {
|
|
|
- const ai = state.lastClip
|
|
|
- if (ai !== a && ai !== b && singleAtomObscures(state, ai, x, y, z)) {
|
|
|
- return ai
|
|
|
- } else {
|
|
|
- state.lastClip = -1
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- for (let j = 0, jl = state.neighbours.count; j < jl; ++j) {
|
|
|
- const ai = OrderedSet.getAt(state.position.indices, state.neighbours.indices[j])
|
|
|
- if (ai !== a && ai !== b && singleAtomObscures(state, ai, x, y, z)) {
|
|
|
- state.lastClip = ai
|
|
|
- return ai
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return -1
|
|
|
-}
|
|
|
+//
|
|
|
|
|
|
-/**
|
|
|
- * `ai` must be a resolved index
|
|
|
- */
|
|
|
-function singleAtomObscures (state: MolSurfCalcState, ai: number, x: number, y: number, z: number) {
|
|
|
- const r = state.position.radius[ai]
|
|
|
- const dx = state.position.x[ai] - x
|
|
|
- const dy = state.position.y[ai] - y
|
|
|
- const dz = state.position.z[ai] - z
|
|
|
- const dSq = dx * dx + dy * dy + dz * dz
|
|
|
- return dSq < (r * r)
|
|
|
+export const MolecularSurfaceCalculationParams = {
|
|
|
+ resolution: PD.Numeric(0.5, { min: 0.01, max: 10, 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
|
|
|
|
|
|
-/**
|
|
|
- * 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
|
|
|
- */
|
|
|
-async function projectPoints (ctx: RuntimeContext, state: MolSurfCalcState) {
|
|
|
- const { position, lookup3d, min, space, data, idData, scaleFactor, updateChunk } = state
|
|
|
- const { gridx, gridy, gridz } = state
|
|
|
-
|
|
|
- const { indices, x, y, z, radius } = position
|
|
|
- const n = OrderedSet.size(indices)
|
|
|
|
|
|
- const [ dimX, dimY, dimZ ] = space.dimensions
|
|
|
- const iu = dimZ, iv = dimY, iuv = iu * iv
|
|
|
+export async function calcMolecularSurface(ctx: RuntimeContext, position: Required<PositionData>, maxRadius: number, props: MolecularSurfaceCalculationProps) {
|
|
|
+ // Field generation method adapted from AstexViewer (Mike Hartshorn) by Fred Ludlow.
|
|
|
+ // Other parts based heavily on NGL (Alexander Rose) EDT Surface class
|
|
|
|
|
|
- for (let i = 0; i < n; ++i) {
|
|
|
- const j = OrderedSet.getAt(indices, i)
|
|
|
- const vx = x[j], vy = y[j], vz = z[j]
|
|
|
- const rad = radius[j]
|
|
|
- const rSq = rad * rad
|
|
|
-
|
|
|
- state.neighbours = 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 - min[0]))
|
|
|
- const iay = Math.floor(scaleFactor * (vy - min[1]))
|
|
|
- const iaz = Math.floor(scaleFactor * (vz - min[2]))
|
|
|
-
|
|
|
- // 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(state, spx, spy, spz, j, -1) === -1) {
|
|
|
- const dd = rad - d
|
|
|
- if (dd < data[idx]) {
|
|
|
- data[idx] = dd
|
|
|
- idData[idx] = i
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+ 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
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- if (i % updateChunk === 0 && ctx.shouldUpdate) {
|
|
|
- await ctx.update({ message: 'projecting points', current: i, max: n })
|
|
|
+ for (let j = 0, jl = neighbours.count; j < jl; ++j) {
|
|
|
+ const ai = OrderedSet.getAt(position.indices, neighbours.indices[j])
|
|
|
+ if (ai !== a && ai !== b && singleAtomObscures(ai, x, y, z)) {
|
|
|
+ lastClip = ai
|
|
|
+ return ai
|
|
|
+ }
|
|
|
}
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// 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 (state: MolSurfCalcState, a: number, b: number) {
|
|
|
- const { position, min, space, data, idData } = state
|
|
|
- const { cosTable, sinTable, probePositions, probeRadius, scaleFactor } = state
|
|
|
- const { gridx, gridy, gridz } = state
|
|
|
-
|
|
|
- const [ dimX, dimY, dimZ ] = space.dimensions
|
|
|
- const iu = dimZ, iv = dimY, iuv = iu * iv
|
|
|
-
|
|
|
- const ng = Math.max(5, 2 + Math.floor(probeRadius * scaleFactor))
|
|
|
-
|
|
|
- const rA = position.radius[a]
|
|
|
- const rB = position.radius[b]
|
|
|
- const dx = atob[0] = position.x[b] - position.x[a]
|
|
|
- const dy = atob[1] = position.y[b] - position.y[a]
|
|
|
- const dz = atob[2] = position.z[b] - position.z[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] + position.x[a]
|
|
|
- mid[1] = atob[1] + position.y[a]
|
|
|
- mid[2] = atob[2] + position.z[a]
|
|
|
-
|
|
|
- state.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]
|
|
|
+ return -1
|
|
|
+ }
|
|
|
|
|
|
- if (obscured(state, px, py, pz, a, b) === -1) {
|
|
|
- const iax = Math.floor(scaleFactor * (px - min[0]))
|
|
|
- const iay = Math.floor(scaleFactor * (py - min[1]))
|
|
|
- const iaz = Math.floor(scaleFactor * (pz - min[2]))
|
|
|
+ /**
|
|
|
+ * `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 = px - gridx[xi]
|
|
|
+ const dx = gridx[xi] - vx
|
|
|
const xIdx = xi * iuv
|
|
|
-
|
|
|
for (let yi = begY; yi < endY; ++yi) {
|
|
|
- const dy = py - gridy[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 = pz - gridz[zi]
|
|
|
+ const dz = gridz[zi] - vz
|
|
|
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] = OrderedSet.indexOf(position.indices, dp < 0.0 ? b : a)
|
|
|
+ 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] = i
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
-}
|
|
|
|
|
|
-async function projectTorii (ctx: RuntimeContext, state: MolSurfCalcState) {
|
|
|
- const { n, lookup3d, position, updateChunk } = state
|
|
|
- const { x, y, z, indices, radius } = position
|
|
|
- for (let i = 0; i < n; ++i) {
|
|
|
- const k = OrderedSet.getAt(indices, i)
|
|
|
- state.neighbours = lookup3d.find(x[k], y[k], z[k], radius[k])
|
|
|
- for (let j = 0, jl = state.neighbours.count; j < jl; ++j) {
|
|
|
- const l = OrderedSet.getAt(indices, state.neighbours.indices[j])
|
|
|
- if (k < l) projectTorus(state, k, l)
|
|
|
+ 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 })
|
|
|
+ }
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- if (i % updateChunk === 0 && ctx.shouldUpdate) {
|
|
|
- await ctx.update({ message: 'projecting torii', 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] = OrderedSet.indexOf(position.indices, dp < 0.0 ? b : a)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
-}
|
|
|
|
|
|
-//
|
|
|
+ async function projectTorii () {
|
|
|
+ for (let i = 0; i < n; ++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)
|
|
|
+ }
|
|
|
|
|
|
-interface MolSurfCalcState {
|
|
|
- /** Cached last value for obscured test */
|
|
|
- lastClip: number
|
|
|
- /** Neighbours as transient result array from lookup3d */
|
|
|
- neighbours: Result<number>
|
|
|
-
|
|
|
- lookup3d: Lookup3D
|
|
|
- position: Required<PositionData>
|
|
|
- min: Vec3
|
|
|
- updateChunk: number
|
|
|
-
|
|
|
- maxRadius: number
|
|
|
-
|
|
|
- n: number
|
|
|
- resolution: number
|
|
|
- scaleFactor: number
|
|
|
- probeRadius: number
|
|
|
-
|
|
|
- /** Angle lookup tables */
|
|
|
- cosTable: Float32Array
|
|
|
- sinTable: Float32Array
|
|
|
- probePositions: number
|
|
|
-
|
|
|
- /** grid lookup tables */
|
|
|
- gridx: Float32Array
|
|
|
- gridy: Float32Array
|
|
|
- gridz: Float32Array
|
|
|
-
|
|
|
- expandedBox: Box3D
|
|
|
- space: Tensor.Space
|
|
|
- data: Tensor.Data
|
|
|
- idData: Tensor.Data
|
|
|
-}
|
|
|
+ if (i % updateChunk === 0 && ctx.shouldUpdate) {
|
|
|
+ await ctx.update({ message: 'projecting torii', current: i, max: n })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
-async function createState(ctx: RuntimeContext, position: Required<PositionData>, maxRadius: number, props: MolecularSurfaceCalculationProps): Promise<MolSurfCalcState> {
|
|
|
+ // 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
|
|
|
const box = lookup3d.boundary.box
|
|
|
- const { indices } = position
|
|
|
+ const { indices, x: px, y: py, z: pz, radius } = position
|
|
|
const n = OrderedSet.size(indices)
|
|
|
|
|
|
const pad = maxRadius * 2 + resolution
|
|
|
const expandedBox = Box3D.expand(Box3D(), box, Vec3.create(pad, pad, pad));
|
|
|
- const min = expandedBox.min
|
|
|
+ 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)
|
|
@@ -367,81 +339,28 @@ async function createState(ctx: RuntimeContext, position: Required<PositionData>
|
|
|
fillUniform(data, -1001.0)
|
|
|
fillUniform(idData, -1)
|
|
|
|
|
|
- const gridx = fillGridDim(dim[0], min[0], resolution)
|
|
|
- const gridy = fillGridDim(dim[1], min[1], resolution)
|
|
|
- const gridz = fillGridDim(dim[2], min[2], resolution)
|
|
|
-
|
|
|
- const updateChunk = Math.ceil(1000000 / (Math.pow(maxRadius, 3) * resolution))
|
|
|
-
|
|
|
- return {
|
|
|
- lastClip: -1,
|
|
|
- neighbours: lookup3d.find(0, 0, 0, 0),
|
|
|
-
|
|
|
- lookup3d,
|
|
|
- position,
|
|
|
- min,
|
|
|
- updateChunk,
|
|
|
-
|
|
|
- maxRadius,
|
|
|
-
|
|
|
- n,
|
|
|
- resolution,
|
|
|
- scaleFactor,
|
|
|
- probeRadius,
|
|
|
-
|
|
|
- cosTable,
|
|
|
- sinTable,
|
|
|
- probePositions,
|
|
|
-
|
|
|
- gridx,
|
|
|
- gridy,
|
|
|
- gridz,
|
|
|
-
|
|
|
- expandedBox,
|
|
|
- space,
|
|
|
- data,
|
|
|
- idData,
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-//
|
|
|
-
|
|
|
-export const MolecularSurfaceCalculationParams = {
|
|
|
- resolution: PD.Numeric(0.5, { min: 0.01, max: 10, 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, props: MolecularSurfaceCalculationProps) {
|
|
|
- // Field generation method adapted from AstexViewer (Mike Hartshorn) by Fred Ludlow.
|
|
|
- // Other parts based heavily on NGL (Alexander Rose) EDT Surface class
|
|
|
-
|
|
|
- console.time('MolecularSurface')
|
|
|
-
|
|
|
- console.time('MolecularSurface createState')
|
|
|
- const state = await createState(ctx, position, maxRadius, props)
|
|
|
- console.timeEnd('MolecularSurface createState')
|
|
|
+ const gridx = fillGridDim(dimX, minX, resolution)
|
|
|
+ const gridy = fillGridDim(dimY, minY, resolution)
|
|
|
+ const gridz = fillGridDim(dimZ, minZ, resolution)
|
|
|
|
|
|
- console.time('MolecularSurface projectPoints')
|
|
|
- await projectPoints(ctx, state)
|
|
|
- console.timeEnd('MolecularSurface projectPoints')
|
|
|
+ const updateChunk = Math.ceil(100000 / ((Math.pow(Math.pow(maxRadius, 3), 3) * scaleFactor)))
|
|
|
+ // console.timeEnd('MolecularSurface createState')
|
|
|
|
|
|
- console.time('MolecularSurface projectTorii')
|
|
|
- await projectTorii(ctx, state)
|
|
|
- console.timeEnd('MolecularSurface projectTorii')
|
|
|
+ // console.time('MolecularSurface projectPoints')
|
|
|
+ await projectPoints()
|
|
|
+ // console.timeEnd('MolecularSurface projectPoints')
|
|
|
|
|
|
- console.timeEnd('MolecularSurface')
|
|
|
+ // console.time('MolecularSurface projectTorii')
|
|
|
+ await projectTorii()
|
|
|
+ // console.timeEnd('MolecularSurface projectTorii')
|
|
|
+ // console.timeEnd('MolecularSurface')
|
|
|
|
|
|
- const field = Tensor.create(state.space, state.data)
|
|
|
- const idField = Tensor.create(state.space, state.idData)
|
|
|
+ const field = Tensor.create(space, data)
|
|
|
+ const idField = Tensor.create(space, idData)
|
|
|
|
|
|
- const { resolution, expandedBox } = state
|
|
|
const transform = Mat4.identity()
|
|
|
Mat4.fromScaling(transform, Vec3.create(resolution, resolution, resolution))
|
|
|
Mat4.setTranslation(transform, expandedBox.min)
|
|
|
- console.log({ field, idField, transform, state })
|
|
|
+ // console.log({ field, idField, transform, updateChunk })
|
|
|
return { field, idField, transform }
|
|
|
}
|