UNPKG

molstar

Version:

A comprehensive macromolecular library.

353 lines 17.9 kB
/** * 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