molstar
Version:
A comprehensive macromolecular library.
470 lines (469 loc) • 20.5 kB
JavaScript
/**
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Dušan Veľký <dvelky@mail.muni.cz>
*/
import { OrderedSet } from '../../../mol-data/int';
import { addSphere } from '../../../mol-geo/geometry/mesh/builder/sphere';
import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder';
import { computeMarchingCubesMesh } from '../../../mol-geo/util/marching-cubes/algorithm';
import { Sphere3D, Box3D, GridLookup3D, fillGridDim } from '../../../mol-math/geometry';
import { getBoundary } from '../../../mol-math/geometry/boundary';
import { DefaultMolecularSurfaceCalculationProps } from '../../../mol-math/geometry/molecular-surface';
import { lerp, spline } from '../../../mol-math/interpolate';
import { Vec3, Tensor, Mat4 } from '../../../mol-math/linear-algebra';
import { Shape } from '../../../mol-model/shape';
import { ensureReasonableResolution } from '../../../mol-repr/structure/visual/util/common';
import { Task } from '../../../mol-task';
import { ValueCell } from '../../../mol-util';
import { Color } from '../../../mol-util/color';
export async function createSpheresShape(options) {
var _a;
const builder = MeshBuilder.createState(512, 512, (_a = options.prev) === null || _a === void 0 ? void 0 : _a.geometry);
const tunnel = options.tunnel;
const processedData = interpolateTunnel(tunnel.data, options.sampleRate);
if (options.showRadii) {
for (let i = 0; i < processedData.length; i += 1) {
const p = processedData[i];
builder.currentGroup = i;
const center = [p.X, p.Y, p.Z];
addSphere(builder, center, p.Radius, options.resolution);
}
}
else {
for (let i = 0; i < processedData.length; i += 1) {
const p = processedData[i];
builder.currentGroup = 0;
const center = [p.X, p.Y, p.Z];
addSphere(builder, center, p.Radius, options.resolution);
}
}
const mesh = MeshBuilder.getMesh(builder);
const name = tunnel.props.highlight_label ?
tunnel.props.highlight_label :
tunnel.props.type && tunnel.props.id ?
`${tunnel.props.type} ${tunnel.props.id}` :
'Tunnel';
if (options.showRadii)
return Shape.create(name, tunnel.props, mesh, () => Color(options.color), () => 1, (i) => `[${processedData[i].X.toFixed(3)}, ${processedData[i].Y.toFixed(3)}, ${processedData[i].Z.toFixed(3)}] - radius: ${processedData[i].Radius.toFixed(3)}`);
return Shape.create(name, tunnel.props, mesh, () => Color(options.color), () => 1, () => name);
}
export async function createTunnelShape(options) {
var _a;
const tunnel = options.tunnel;
const mesh = await createTunnelMesh(tunnel.data, {
detail: options.resolution,
sampleRate: options.sampleRate,
webgl: options.webgl,
prev: (_a = options.prev) === null || _a === void 0 ? void 0 : _a.geometry
});
const name = tunnel.props.highlight_label ?
tunnel.props.highlight_label :
tunnel.props.type && tunnel.props.id ?
`${tunnel.props.type} ${tunnel.props.id}` :
'Tunnel';
return Shape.create(name, tunnel.props, mesh, () => Color(options.color), () => 1, () => name);
}
function profileToVec3(profile) {
return Vec3.create(profile.X, profile.Y, profile.Z);
}
// Centripetal Catmull–Rom spline interpolation
function interpolateTunnel(profile, sampleRate) {
const interpolatedProfiles = [];
if (profile.length < 4)
return profile; // Ensuring there are enough points to interpolate
interpolatedProfiles.push(profile[0]);
let lastPoint = profileToVec3(profile[0]);
let currentDistance = 0;
const pointInterval = 1 / sampleRate;
for (let i = 1; i < profile.length - 2; i++) {
const P0 = profile[i - 1];
const P1 = profile[i];
const P2 = profile[i + 1];
const P3 = profile[i + 2];
for (let t = 0; t <= 1; t += 0.05) {
const interpolatedX = spline(P0.X, P1.X, P2.X, P3.X, t, 0.5);
const interpolatedY = spline(P0.Y, P1.Y, P2.Y, P3.Y, t, 0.5);
const interpolatedZ = spline(P0.Z, P1.Z, P2.Z, P3.Z, t, 0.5);
const interpolatedPoint = Vec3.create(interpolatedX, interpolatedY, interpolatedZ);
const distanceToAdd = Vec3.distance(lastPoint, interpolatedPoint);
currentDistance += distanceToAdd;
if (currentDistance >= pointInterval) {
interpolatedProfiles.push({
X: interpolatedX,
Y: interpolatedY,
Z: interpolatedZ,
Radius: spline(P0.Radius, P1.Radius, P2.Radius, P3.Radius, t, 0.5),
Charge: lerp(P1.Charge, P2.Charge, t),
FreeRadius: spline(P0.FreeRadius, P1.FreeRadius, P2.FreeRadius, P3.FreeRadius, t, 0.5),
T: lerp(P1.T, P2.T, t),
Distance: lerp(P1.Distance, P2.Distance, t)
});
lastPoint = interpolatedPoint;
currentDistance -= pointInterval;
}
}
}
// Ensuring the last profile point is included
interpolatedProfiles.push(profile[profile.length - 1]);
return interpolatedProfiles;
}
function convertToPositionData(profile, probeRadius) {
let position = {};
const x = [];
const y = [];
const z = [];
const indices = [];
const radius = [];
let maxRadius = Number.MIN_SAFE_INTEGER;
let sphereCounter = 0;
for (const sphere of profile) {
x.push(sphere.X);
y.push(sphere.Y);
z.push(sphere.Z);
indices.push(sphereCounter);
radius.push(sphere.Radius + probeRadius);
if (sphere.Radius > maxRadius)
maxRadius = sphere.Radius;
sphereCounter++;
}
position = { x, y, z, indices: OrderedSet.ofSortedArray(indices), radius, id: indices };
return position;
}
async function createTunnelMesh(data, options) {
const props = {
...DefaultMolecularSurfaceCalculationProps,
};
const preprocessedData = interpolateTunnel(data, options.sampleRate);
const positions = convertToPositionData(preprocessedData, props.probeRadius);
const bounds = getBoundary(positions);
let maxR = 0;
for (let i = 0; i < positions.radius.length; ++i) {
const r = positions.radius[i];
if (maxR < r)
maxR = r;
}
const p = ensureReasonableResolution(bounds.box, props);
const { field, transform, /* resolution,*/ maxRadius, /* idField */ } = await computeTunnelSurface({
positions,
boundary: bounds,
maxRadius: maxR,
box: bounds.box,
props: p
}).run();
const params = {
isoLevel: p.probeRadius,
scalarField: field,
};
const surface = await computeMarchingCubesMesh(params, options.prev).run();
const iterations = Math.ceil(2 / 1);
Mesh.smoothEdges(surface, { iterations, maxNewEdgeLength: Math.sqrt(2) });
Mesh.transform(surface, transform);
if (options.webgl && !options.webgl.isWebGL2) {
Mesh.uniformTriangleGroup(surface);
ValueCell.updateIfChanged(surface.varyingGroup, false);
}
else {
ValueCell.updateIfChanged(surface.varyingGroup, true);
}
const sphere = Sphere3D.expand(Sphere3D(), bounds.sphere, maxRadius);
surface.setBoundingSphere(sphere);
surface.meta.resolution = options.detail;
return surface;
}
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 computeTunnelSurface(surfaceData) {
return Task.create('Tunnel Surface', async (ctx) => {
return await calcTunnelSurface(ctx, surfaceData);
});
}
function getAngleTables(probePositions) {
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 };
}
// From '../../../\mol-math\geometry\molecular-surface.ts'
async function calcTunnelSurface(ctx, surfaceData) {
// 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, y, z, a, b) {
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, x, y, z) {
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, endI) {
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, b) {
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, endI) {
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 } = surfaceData.props;
const scaleFactor = 1 / resolution;
const ngTorus = Math.max(5, 2 + Math.floor(probeRadius * scaleFactor));
const cellSize = Vec3.create(surfaceData.maxRadius, surfaceData.maxRadius, surfaceData.maxRadius);
Vec3.scale(cellSize, cellSize, 2);
const lookup3d = GridLookup3D(surfaceData.positions, surfaceData.boundary, cellSize);
const neighbours = lookup3d.result;
if (surfaceData.box === null)
surfaceData.box = lookup3d.boundary.box;
const { indices, x: px, y: py, z: pz, id, radius } = surfaceData.positions;
const n = OrderedSet.size(indices);
const pad = surfaceData.maxRadius + resolution;
const expandedBox = Box3D.expand(Box3D(), surfaceData.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(surfaceData.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, resolution, maxRadius: surfaceData.maxRadius };
}