molstar
Version:
A comprehensive macromolecular library.
353 lines • 17.9 kB
JavaScript
/**
* 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 { __assign, __awaiter, __generator } from "tslib";
import { Vec3, Tensor } from '../../mol-math/linear-algebra';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { OrderedSet } from '../../mol-data/int';
import { Mat4 } from '../../mol-math/linear-algebra/3d/mat4';
import { Box3D, GridLookup3D, fillGridDim } from '../../mol-math/geometry';
import { BaseGeometry } from '../../mol-geo/geometry/base';
function normalToLine(out, p) {
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;
}
function getAngleTables(probePositions) {
var theta = 0.0;
var step = 2 * Math.PI / probePositions;
var cosTable = new Float32Array(probePositions);
var sinTable = new Float32Array(probePositions);
for (var i = 0; i < probePositions; i++) {
cosTable[i] = Math.cos(theta);
sinTable[i] = Math.sin(theta);
theta += step;
}
return { cosTable: cosTable, sinTable: sinTable };
}
//
export var MolecularSurfaceCalculationParams = {
probeRadius: PD.Numeric(1.4, { min: 0, max: 10, step: 0.1 }, { description: 'Radius of the probe tracing the molecular surface.' }),
resolution: PD.Numeric(0.5, { min: 0.01, max: 20, step: 0.01 }, __assign({ description: 'Grid resolution/cell spacing.' }, BaseGeometry.CustomQualityParamInfo)),
probePositions: PD.Numeric(36, { min: 12, max: 90, step: 1 }, __assign({ description: 'Number of positions tested for probe target intersection.' }, BaseGeometry.CustomQualityParamInfo)),
};
export var DefaultMolecularSurfaceCalculationProps = PD.getDefaultValues(MolecularSurfaceCalculationParams);
export function calcMolecularSurface(ctx, position, boundary, maxRadius, box, props) {
return __awaiter(this, void 0, void 0, function () {
/**
* 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, y, z, a, b) {
if (lastClip !== -1) {
var ai = lastClip;
if (ai !== a && ai !== b && singleAtomObscures(ai, x, y, z)) {
return ai;
}
else {
lastClip = -1;
}
}
for (var j = 0, jl = neighbours.count; j < jl; ++j) {
var 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, x, y, z) {
var r = radius[ai];
var dx = px[ai] - x;
var dy = py[ai] - y;
var dz = pz[ai] - z;
var 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, endI) {
for (var i = begI; i < endI; ++i) {
var j = OrderedSet.getAt(indices, i);
var vx = px[j], vy = py[j], vz = pz[j];
var rad = radius[j];
var rSq = rad * rad;
lookup3d.find(vx, vy, vz, rad);
// Number of grid points, round this up...
var ng = Math.ceil(rad * scaleFactor);
// Center of the atom, mapped to grid points (take floor)
var iax = Math.floor(scaleFactor * (vx - minX));
var iay = Math.floor(scaleFactor * (vy - minY));
var iaz = Math.floor(scaleFactor * (vz - minZ));
// Extents of grid to consider for this atom
var begX = Math.max(0, iax - ng);
var begY = Math.max(0, iay - ng);
var 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)
var endX = Math.min(dimX, iax + ng + 2);
var endY = Math.min(dimY, iay + ng + 2);
var endZ = Math.min(dimZ, iaz + ng + 2);
for (var xi = begX; xi < endX; ++xi) {
var dx = gridx[xi] - vx;
var xIdx = xi * iuv;
for (var yi = begY; yi < endY; ++yi) {
var dy = gridy[yi] - vy;
var dxySq = dx * dx + dy * dy;
var xyIdx = yi * iu + xIdx;
for (var zi = begZ; zi < endZ; ++zi) {
var dz = gridz[zi] - vz;
var dSq = dxySq + dz * dz;
if (dSq < rSq) {
var 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 )
var d = Math.sqrt(dSq);
var ap = rad / d;
var spx = dx * ap + vx;
var spy = dy * ap + vy;
var spz = dz * ap + vz;
if (obscured(spx, spy, spz, j, -1) === -1) {
var dd = rad - d;
if (dd < data[idx]) {
data[idx] = dd;
idData[idx] = id[i];
}
}
}
}
}
}
}
}
function projectPoints() {
return __awaiter(this, void 0, void 0, function () {
var i;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
i = 0;
_a.label = 1;
case 1:
if (!(i < n)) return [3 /*break*/, 4];
projectPointsRange(i, Math.min(i + updateChunk, n));
if (!ctx.shouldUpdate) return [3 /*break*/, 3];
return [4 /*yield*/, ctx.update({ message: 'projecting points', current: i, max: n })];
case 2:
_a.sent();
_a.label = 3;
case 3:
i += updateChunk;
return [3 /*break*/, 1];
case 4: return [2 /*return*/];
}
});
});
}
/**
* `a` and `b` must be resolved indices
*/
function projectTorus(a, b) {
var rA = radius[a];
var rB = radius[b];
var dx = atob[0] = px[b] - px[a];
var dy = atob[1] = py[b] - py[a];
var dz = atob[2] = pz[b] - pz[a];
var dSq = dx * dx + dy * dy + dz * dz;
// This check now redundant as already done in AVHash.withinRadii
// if (dSq > ((rA + rB) * (rA + rB))) { return }
var d = Math.sqrt(dSq);
// Find angle between a->b vector and the circle
// of their intersection by cosine rule
var cosA = (rA * rA + d * d - rB * rB) / (2.0 * rA * d);
// distance along a->b at intersection
var 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
var 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 (var i = 0; i < probePositions; ++i) {
var cost = cosTable[i];
var sint = sinTable[i];
var px_1 = mid[0] + cost * n1[0] + sint * n2[0];
var py_1 = mid[1] + cost * n1[1] + sint * n2[1];
var pz_1 = mid[2] + cost * n1[2] + sint * n2[2];
if (obscured(px_1, py_1, pz_1, a, b) === -1) {
var iax = Math.floor(scaleFactor * (px_1 - minX));
var iay = Math.floor(scaleFactor * (py_1 - minY));
var iaz = Math.floor(scaleFactor * (pz_1 - minZ));
var begX = Math.max(0, iax - ngTorus);
var begY = Math.max(0, iay - ngTorus);
var begZ = Math.max(0, iaz - ngTorus);
var endX = Math.min(dimX, iax + ngTorus + 2);
var endY = Math.min(dimY, iay + ngTorus + 2);
var endZ = Math.min(dimZ, iaz + ngTorus + 2);
for (var xi = begX; xi < endX; ++xi) {
var dx_1 = px_1 - gridx[xi];
var xIdx = xi * iuv;
for (var yi = begY; yi < endY; ++yi) {
var dy_1 = py_1 - gridy[yi];
var dxySq = dx_1 * dx_1 + dy_1 * dy_1;
var xyIdx = yi * iu + xIdx;
for (var zi = begZ; zi < endZ; ++zi) {
var dz_1 = pz_1 - gridz[zi];
var dSq_1 = dxySq + dz_1 * dz_1;
var idx = zi + xyIdx;
var current = data[idx];
if (current > 0.0 && dSq_1 < (current * current)) {
data[idx] = Math.sqrt(dSq_1);
// Is this grid point closer to a or b?
// Take dot product of atob and gridpoint->p (dx, dy, dz)
var dp = dx_1 * atob[0] + dy_1 * atob[1] + dz_1 * atob[2];
idData[idx] = id[OrderedSet.indexOf(indices, dp < 0.0 ? b : a)];
}
}
}
}
}
}
}
function projectToriiRange(begI, endI) {
for (var i = begI; i < endI; ++i) {
var k = OrderedSet.getAt(indices, i);
lookup3d.find(px[k], py[k], pz[k], radius[k]);
for (var j = 0, jl = neighbours.count; j < jl; ++j) {
var l = OrderedSet.getAt(indices, neighbours.indices[j]);
if (k < l)
projectTorus(k, l);
}
}
}
function projectTorii() {
return __awaiter(this, void 0, void 0, function () {
var i;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
i = 0;
_a.label = 1;
case 1:
if (!(i < n)) return [3 /*break*/, 4];
projectToriiRange(i, Math.min(i + updateChunk, n));
if (!ctx.shouldUpdate) return [3 /*break*/, 3];
return [4 /*yield*/, ctx.update({ message: 'projecting torii', current: i, max: n })];
case 2:
_a.sent();
_a.label = 3;
case 3:
i += updateChunk;
return [3 /*break*/, 1];
case 4: return [2 /*return*/];
}
});
});
}
var lastClip, atob, mid, n1, n2, resolution, probeRadius, probePositions, scaleFactor, ngTorus, cellSize, lookup3d, neighbours, indices, px, py, pz, id, radius, n, pad, expandedBox, _a, minX, minY, minZ, scaledBox, dim, dimX, dimY, dimZ, iu, iv, iuv, _b, cosTable, sinTable, space, data, idData, gridx, gridy, gridz, updateChunk, field, idField, transform;
return __generator(this, function (_c) {
switch (_c.label) {
case 0:
lastClip = -1;
atob = Vec3();
mid = Vec3();
n1 = Vec3();
n2 = Vec3();
resolution = props.resolution, probeRadius = props.probeRadius, probePositions = props.probePositions;
scaleFactor = 1 / resolution;
ngTorus = Math.max(5, 2 + Math.floor(probeRadius * scaleFactor));
cellSize = Vec3.create(maxRadius, maxRadius, maxRadius);
Vec3.scale(cellSize, cellSize, 2);
lookup3d = GridLookup3D(position, boundary, cellSize);
neighbours = lookup3d.result;
if (box === null)
box = lookup3d.boundary.box;
indices = position.indices, px = position.x, py = position.y, pz = position.z, id = position.id, radius = position.radius;
n = OrderedSet.size(indices);
pad = maxRadius + resolution;
expandedBox = Box3D.expand(Box3D(), box, Vec3.create(pad, pad, pad));
_a = expandedBox.min, minX = _a[0], minY = _a[1], minZ = _a[2];
scaledBox = Box3D.scale(Box3D(), expandedBox, scaleFactor);
dim = Box3D.size(Vec3(), scaledBox);
Vec3.ceil(dim, dim);
dimX = dim[0], dimY = dim[1], dimZ = dim[2];
iu = dimZ, iv = dimY, iuv = iu * iv;
_b = getAngleTables(probePositions), cosTable = _b.cosTable, sinTable = _b.sinTable;
space = Tensor.Space(dim, [0, 1, 2], Float32Array);
data = space.create();
idData = space.create();
data.fill(-1001.0);
idData.fill(-1);
gridx = fillGridDim(dimX, minX, resolution);
gridy = fillGridDim(dimY, minY, resolution);
gridz = fillGridDim(dimZ, minZ, resolution);
updateChunk = Math.ceil(100000 / ((Math.pow(Math.pow(maxRadius, 3), 3) * scaleFactor)));
// console.timeEnd('MolecularSurface createState')
// console.time('MolecularSurface projectPoints')
return [4 /*yield*/, projectPoints()];
case 1:
// console.timeEnd('MolecularSurface createState')
// console.time('MolecularSurface projectPoints')
_c.sent();
// console.timeEnd('MolecularSurface projectPoints')
// console.time('MolecularSurface projectTorii')
return [4 /*yield*/, projectTorii()];
case 2:
// console.timeEnd('MolecularSurface projectPoints')
// console.time('MolecularSurface projectTorii')
_c.sent();
field = Tensor.create(space, data);
idField = Tensor.create(space, idData);
transform = Mat4.identity();
Mat4.fromScaling(transform, Vec3.create(resolution, resolution, resolution));
Mat4.setTranslation(transform, expandedBox.min);
// console.log({ field, idField, transform, updateChunk })
return [2 /*return*/, { field: field, idField: idField, transform: transform, resolution: resolution }];
}
});
});
}
//# sourceMappingURL=molecular-surface.js.map