UNPKG

pdbe-molstar

Version:
203 lines (202 loc) 9.12 kB
"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 }); }