pdbe-molstar
Version:
Molstar implementation for PDBe
203 lines (202 loc) • 9.12 kB
JavaScript
"use strict";
/** Helper functions to allow visualizing Foldseek results and superposing them on the query structure */
Object.defineProperty(exports, "__esModule", { value: true });
exports.loadFoldseekSuperposition = loadFoldseekSuperposition;
exports.exportModels = exportModels;
const tslib_1 = require("tslib");
const export_1 = require("molstar/lib/extensions/model-export/export");
const minimize_rmsd_1 = require("molstar/lib/mol-math/linear-algebra/3d/minimize-rmsd");
const __1 = require("../..");
const helpers_1 = require("../../helpers");
const superposition_1 = require("../../superposition");
/** Load target structure as defined by `apiData`
* and superpose it on the already loaded query structure. */
function loadFoldseekSuperposition(viewer_1, targetStructId_1, apiData_1) {
return tslib_1.__awaiter(this, arguments, void 0, function* (viewer, targetStructId, apiData, targetColor = '#00ff00') {
var _a, _b, _c, _d;
const Q_STRUCT_ID = __1.PDBeMolstarPlugin.MAIN_STRUCTURE_ID;
// Retrieve target entry ID and chain ID
let tEntryId;
let tAuthAsymId;
if (apiData.database === 'pdb') {
[tEntryId, tAuthAsymId] = apiData.target.split('_');
}
else if (apiData.database === 'afdb50') {
tEntryId = apiData.target;
tAuthAsymId = 'A';
}
else {
throw new Error(`Unknown database: ${apiData.database}`);
}
// Load target structure
let tUrl;
let tFormat;
let tBinary;
if (apiData.tstructure) {
tUrl = apiData.tstructure.url;
tFormat = (apiData.tstructure.format === 'cif' || apiData.tstructure.format === 'bcif') ? 'mmcif' : apiData.tstructure.format;
tBinary = apiData.tstructure.binary;
}
else {
if (apiData.database === 'pdb') {
tUrl = (0, helpers_1.getStructureUrl)(viewer.initParams, { pdbId: tEntryId, queryType: 'atoms', queryParams: { auth_asym_id: tAuthAsymId } });
tFormat = 'mmcif';
tBinary = viewer.initParams.encoding === 'bcif';
}
else if (apiData.database === 'afdb50') {
throw new Error("tstructure must be provided when database==='afdb50'");
}
else {
throw new Error(`Unknown database: ${apiData.database}`);
}
}
yield viewer.load({ url: tUrl, format: tFormat, isBinary: tBinary, id: targetStructId }, false);
// Retrieve structure data
const qStructure = (_b = (_a = viewer.getStructure(Q_STRUCT_ID)) === null || _a === void 0 ? void 0 : _a.cell.obj) === null || _b === void 0 ? void 0 : _b.data;
const tStructure = (_d = (_c = viewer.getStructure(targetStructId)) === null || _c === void 0 ? void 0 : _c.cell.obj) === null || _d === void 0 ? void 0 : _d.data;
if (!qStructure)
throw new Error('Query structure not loaded');
if (!tStructure)
throw new Error('Target structure not loaded');
// Decipher Foldseek alignment and produce transformation matrix
const [qMatchedIndicesInChain, tMatchedIndicesInChain] = getMatchedResidues(apiData.qstart - 1, apiData.qaln, apiData.tstart - 1, apiData.taln); // subtracting 1 to get 0-based indices
const qLabelAsymId = 'A';
const tLabelAsymId = auth_asym_id_to_label_asym_id(tStructure, tAuthAsymId);
const [qCoords, tCoords] = discardMissingLocations(getCoordsForResiduesInChain(qStructure, qMatchedIndicesInChain, qLabelAsymId), getCoordsForResiduesInChain(tStructure, tMatchedIndicesInChain, tLabelAsymId));
const superposition = minimize_rmsd_1.MinimizeRmsd.compute({ a: qCoords, b: tCoords });
// Transform, color, focus
yield (0, superposition_1.transform)(viewer.plugin, viewer.getStructure(targetStructId).cell, superposition.bTransform);
yield viewer.visual.select({ data: [{ color: targetColor }], structureId: targetStructId }); // color target
yield viewer.visual.reset({ camera: true });
return Object.assign(Object.assign({}, superposition), { nAligned: qCoords.x.length });
});
}
function getMatchedResidues(qstart, qaln, tstart, taln) {
if (qaln.length !== taln.length) {
throw new Error('Length of qaln and taln must be the same');
}
const n = qaln.length;
let q = qstart;
let t = tstart;
const qResis = [];
const tResis = [];
for (let i = 0; i < n; i++) {
if (qaln[i] === '-') {
// gap in query
t++;
}
else if (taln[i] === '-') {
// gap in target
q++;
}
else {
// match
qResis.push(q);
tResis.push(t);
t++;
q++;
}
}
return [qResis, tResis];
}
function getCoordsForResiduesInChain(structure, residueIndicesInChain, label_asym_id) {
const resRangeForChain = residueIndexRangeForChain(structure, label_asym_id);
const residueIndices = residueIndicesInChain.map(i => resRangeForChain.start + i);
if (residueIndices[residueIndices.length - 1] >= resRangeForChain.end)
throw new Error('Residues out of range');
const coords = getCACoordsForResidues(structure, residueIndices);
return coords;
}
function residueIndexRangeForChain(struct, label_asym_id) {
const h = struct.model.atomicHierarchy;
const iChain = Array.from(h.chains.label_asym_id.toArray()).indexOf(label_asym_id);
const fromAtom = h.chainAtomSegments.offsets[iChain];
const toAtom = h.chainAtomSegments.offsets[iChain + 1];
const fromResidue = h.residueAtomSegments.index[fromAtom];
const toResidue = h.residueAtomSegments.index[toAtom - 1] + 1;
return { start: fromResidue, end: toResidue };
}
function auth_asym_id_to_label_asym_id(struct, auth_asym_id) {
const entities = struct.model.entities.data;
const nEntities = entities._rowCount;
const chains = struct.model.atomicHierarchy.chains;
const nChains = chains._rowCount;
const polymerEntityIds = new Set();
for (let i = 0; i < nEntities; i++) {
if (entities.type.value(i) === 'polymer') {
polymerEntityIds.add(entities.id.value(i));
}
}
for (let i = 0; i < nChains; i++) {
if (chains.auth_asym_id.value(i) === auth_asym_id && polymerEntityIds.has(chains.label_entity_id.value(i))) {
return chains.label_asym_id.value(i);
}
}
throw new Error(`There is no polymer chain with auth_asym_id ${auth_asym_id}.`);
}
function getCACoordsForResidues(struct, residueIndices) {
const hier = struct.model.atomicHierarchy;
const conf = struct.model.atomicConformation;
const n = residueIndices.length;
const coords = minimize_rmsd_1.MinimizeRmsd.Positions.empty(n);
for (let i = 0; i < n; i++) {
const iRes = residueIndices[i];
const fromAtom = hier.residueAtomSegments.offsets[iRes];
const toAtom = hier.residueAtomSegments.offsets[iRes + 1];
let theAtom = -1;
for (let iAtom = fromAtom; iAtom < toAtom; iAtom++) {
const atomName = hier.atoms.label_atom_id.value(iAtom);
if (atomName === 'CA') {
theAtom = iAtom;
break;
}
}
if (theAtom >= 0) {
coords.x[i] = conf.x[theAtom];
coords.y[i] = conf.y[theAtom];
coords.z[i] = conf.z[theAtom];
}
else {
const label_seq_id = hier.residues.label_seq_id.value(iRes);
console.warn(`Foldseek extension: C alpha not found for residue ${label_seq_id} in ${struct.model.entry}. Ignoring this residue.`);
coords.x[i] = NaN;
coords.y[i] = NaN;
coords.z[i] = NaN;
}
}
return coords;
}
/** Discard those positions in `a` and `b` where either of them contains NaN. Return original arrays if there are no NaNs. */
function discardMissingLocations(a, b) {
const n = a.x.length;
if (b.x.length !== n)
throw new Error('ValueError: Positions a and b must have the same length');
const positionsOK = [];
for (let i = 0; i < n; i++) {
if (!isNaN(a.x[i]) && !isNaN(b.x[i])) {
positionsOK.push(i);
}
}
const nOK = positionsOK.length;
if (nOK === n) {
return [a, b];
}
else {
const aOK = minimize_rmsd_1.MinimizeRmsd.Positions.empty(nOK);
const bOK = minimize_rmsd_1.MinimizeRmsd.Positions.empty(nOK);
for (let j = 0; j < nOK; j++) {
const i = positionsOK[j];
aOK.x[j] = a.x[i];
aOK.y[j] = a.y[i];
aOK.z[j] = a.z[i];
bOK.x[j] = b.x[i];
bOK.y[j] = b.y[i];
bOK.z[j] = b.z[i];
}
return [aOK, bOK];
}
}
/** Export currently loaded models in mmCIF (or BCIF). Pack in a ZIP if there is more then 1 model. */
function exportModels(viewer, format = 'cif') {
return (0, export_1.exportHierarchy)(viewer.plugin, { format });
}