UNPKG

molstar

Version:

A comprehensive macromolecular library.

451 lines (450 loc) 20.3 kB
/** * Copyright (c) 2020-2025 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> */ import { ParamDefinition as PD } from '../../../mol-util/param-definition'; import { Structure, Unit } from '../../../mol-model/structure'; import { CustomModelProperty } from '../../../mol-model-props/common/custom-model-property'; import { Model } from '../../../mol-model/structure/model'; import { IntAdjacencyGraph } from '../../../mol-math/graph'; import { CustomStructureProperty } from '../../../mol-model-props/common/custom-structure-property'; import { InterUnitGraph } from '../../../mol-math/graph/inter-unit-graph'; import { IntMap, SortedArray } from '../../../mol-data/int'; import { arrayMax } from '../../../mol-util/array'; import { equalEps } from '../../../mol-math/linear-algebra/3d/common'; import { Vec3 } from '../../../mol-math/linear-algebra'; import { QuerySymbolRuntime } from '../../../mol-script/runtime/query/compiler'; import { CustomPropSymbol } from '../../../mol-script/language/symbol'; import { Type } from '../../../mol-script/language/type'; import { Asset } from '../../../mol-util/assets'; import { CustomPropertyDescriptor } from '../../../mol-model/custom-property'; export { ValidationReport }; var ValidationReport; (function (ValidationReport) { let Tag; (function (Tag) { Tag["DensityFit"] = "rcsb-density-fit"; Tag["GeometryQuality"] = "rcsb-geometry-quality"; Tag["RandomCoilIndex"] = "rcsb-random-coil-index"; Tag["Clashes"] = "rcsb-clashes"; })(Tag = ValidationReport.Tag || (ValidationReport.Tag = {})); ValidationReport.DefaultBaseUrl = 'https://files.rcsb.org/pub/pdb/validation_reports'; function getEntryUrl(pdbId, baseUrl) { const id = pdbId.toLowerCase(); return `${baseUrl}/${id.substr(1, 2)}/${id}/${id}_validation.xml.gz`; } ValidationReport.getEntryUrl = getEntryUrl; function isApplicable(model) { return !!model && Model.hasPdbId(model) && !Model.isIntegrative(model); } ValidationReport.isApplicable = isApplicable; function fromXml(xml, model) { return parseValidationReportXml(xml, model); } ValidationReport.fromXml = fromXml; async function fetch(ctx, model, props) { const url = Asset.getUrlAsset(ctx.assetManager, getEntryUrl(model.entryId, props.baseUrl)); const xml = await ctx.assetManager.resolve(url, 'xml').runInContext(ctx.runtime); return { value: fromXml(xml.data, model), assets: [xml] }; } ValidationReport.fetch = fetch; async function open(ctx, model, props) { if (props.input === null) throw new Error('No file given'); const xml = await ctx.assetManager.resolve(props.input, 'xml').runInContext(ctx.runtime); return { value: fromXml(xml.data, model), assets: [xml] }; } ValidationReport.open = open; async function obtain(ctx, model, props) { switch (props.source.name) { case 'file': return open(ctx, model, props.source.params); case 'server': return fetch(ctx, model, props.source.params); } } ValidationReport.obtain = obtain; ValidationReport.symbols = { hasClash: QuerySymbolRuntime.Dynamic(CustomPropSymbol('rcsb', 'validation-report.has-clash', Type.Bool), ctx => { const { unit, element } = ctx.element; if (!Unit.isAtomic(unit)) return 0; const validationReport = ValidationReportProvider.get(unit.model).value; return validationReport && validationReport.clashes.getVertexEdgeCount(element) > 0; }), issueCount: QuerySymbolRuntime.Dynamic(CustomPropSymbol('rcsb', 'validation-report.issue-count', Type.Num), ctx => { var _a; const { unit, element } = ctx.element; if (!Unit.isAtomic(unit)) return 0; const validationReport = ValidationReportProvider.get(unit.model).value; return ((_a = validationReport === null || validationReport === void 0 ? void 0 : validationReport.geometryIssues.get(unit.residueIndex[element])) === null || _a === void 0 ? void 0 : _a.size) || 0; }), }; })(ValidationReport || (ValidationReport = {})); const FileSourceParams = { input: PD.File({ accept: '.xml,.gz,.zip' }) }; const ServerSourceParams = { baseUrl: PD.Text(ValidationReport.DefaultBaseUrl, { description: 'Base URL to directory tree' }) }; export const ValidationReportParams = { source: PD.MappedStatic('server', { 'file': PD.Group(FileSourceParams, { label: 'File', isFlat: true }), 'server': PD.Group(ServerSourceParams, { label: 'Server', isFlat: true }), }, { options: [['file', 'File'], ['server', 'Server']] }) }; export const ValidationReportProvider = CustomModelProperty.createProvider({ label: 'Validation Report', descriptor: CustomPropertyDescriptor({ name: 'rcsb_validation_report', symbols: ValidationReport.symbols }), type: 'dynamic', defaultParams: ValidationReportParams, getParams: (data) => ValidationReportParams, isApplicable: (data) => ValidationReport.isApplicable(data), obtain: async (ctx, data, props) => { const p = { ...PD.getDefaultValues(ValidationReportParams), ...props }; return await ValidationReport.obtain(ctx, data, p); } }); function createInterUnitClashes(structure, clashes) { const builder = new InterUnitGraph.Builder(); const { a, b, edgeProps: { id, magnitude, distance } } = clashes; const pA = Vec3(), pB = Vec3(); Structure.eachUnitPair(structure, (unitA, unitB) => { const elementsA = unitA.elements; const elementsB = unitB.elements; builder.startUnitPair(unitA.id, unitB.id); for (let i = 0, il = clashes.edgeCount * 2; i < il; ++i) { // TODO create lookup const indexA = SortedArray.indexOf(elementsA, a[i]); const indexB = SortedArray.indexOf(elementsB, b[i]); if (indexA !== -1 && indexB !== -1) { unitA.conformation.position(a[i], pA); unitB.conformation.position(b[i], pB); // check actual distance to avoid clashes between unrelated chain instances if (equalEps(distance[i], Vec3.distance(pA, pB), 0.1)) { builder.add(indexA, indexB, { id: id[i], magnitude: magnitude[i], distance: distance[i], }); } } } builder.finishUnitPair(); }, { maxRadius: arrayMax(clashes.edgeProps.distance), validUnit: (unit) => Unit.isAtomic(unit), validUnitPair: (unitA, unitB) => unitA.model === unitB.model }); return new InterUnitGraph(builder.getMap()); } function createIntraUnitClashes(unit, clashes) { const aIndices = []; const bIndices = []; const ids = []; const magnitudes = []; const distances = []; const pA = Vec3(), pB = Vec3(); const { elements } = unit; const { a, b, edgeCount, edgeProps } = clashes; for (let i = 0, il = edgeCount * 2; i < il; ++i) { // TODO create lookup const indexA = SortedArray.indexOf(elements, a[i]); const indexB = SortedArray.indexOf(elements, b[i]); if (indexA !== -1 && indexB !== -1) { unit.conformation.position(a[i], pA); unit.conformation.position(b[i], pB); // check actual distance to avoid clashes between unrelated chain instances if (equalEps(edgeProps.distance[i], Vec3.distance(pA, pB), 0.1)) { aIndices.push(indexA); bIndices.push(indexB); ids.push(edgeProps.id[i]); magnitudes.push(edgeProps.magnitude[i]); distances.push(edgeProps.distance[i]); } } } const builder = new IntAdjacencyGraph.EdgeBuilder(elements.length, aIndices, bIndices); const id = new Int32Array(builder.slotCount); const magnitude = new Float32Array(builder.slotCount); const distance = new Float32Array(builder.slotCount); for (let i = 0, _i = builder.edgeCount; i < _i; i++) { builder.addNextEdge(); builder.assignProperty(id, ids[i]); builder.assignProperty(magnitude, magnitudes[i]); builder.assignProperty(distance, distances[i]); } return builder.createGraph({ id, magnitude, distance }); } function createClashes(structure, clashes) { const intraUnit = IntMap.Mutable(); for (let i = 0, il = structure.unitSymmetryGroups.length; i < il; ++i) { const group = structure.unitSymmetryGroups[i]; if (!Unit.isAtomic(group.units[0])) continue; const intraClashes = createIntraUnitClashes(group.units[0], clashes); for (let j = 0, jl = group.units.length; j < jl; ++j) { intraUnit.set(group.units[j].id, intraClashes); } } return { interUnit: createInterUnitClashes(structure, clashes), intraUnit }; } export const ClashesProvider = CustomStructureProperty.createProvider({ label: 'Clashes', descriptor: CustomPropertyDescriptor({ name: 'rcsb_clashes', // TODO `cifExport` and `symbol` }), type: 'local', defaultParams: {}, getParams: (data) => ({}), isApplicable: (data) => true, obtain: async (ctx, data) => { await ValidationReportProvider.attach(ctx, data.models[0]); const validationReport = ValidationReportProvider.get(data.models[0]).value; return { value: createClashes(data, validationReport.clashes) }; } }); // function getItem(a, name) { const item = a.getNamedItem(name); return item !== null ? item.value : ''; } function hasAttr(a, name, value) { const item = a.getNamedItem(name); return item !== null && item.value === value; } function getMogInfo(a) { return { mean: parseFloat(getItem(a, 'mean')), obs: parseFloat(getItem(a, 'obsval')), stdev: parseFloat(getItem(a, 'stdev')), z: parseFloat(getItem(a, 'Zscore')), }; } function getMolInfo(a) { return { mean: parseFloat(getItem(a, 'mean')), obs: parseFloat(getItem(a, 'obs')), stdev: parseFloat(getItem(a, 'stdev')), z: parseInt(getItem(a, 'z')), }; } function addIndex(index, element, map) { if (map.has(element)) map.get(element).push(index); else map.set(element, [index]); } function ClashesBuilder(elementsCount) { const aIndices = []; const bIndices = []; const ids = []; const magnitudes = []; const distances = []; const seen = new Map(); return { add(element, id, magnitude, distance, isSymop) { const hash = `${id}|${isSymop ? 's' : ''}`; const other = seen.get(hash); if (other !== undefined) { aIndices[aIndices.length] = element; bIndices[bIndices.length] = other; ids[ids.length] = id; magnitudes[magnitudes.length] = magnitude; distances[distances.length] = distance; } else { seen.set(hash, element); } }, get() { const builder = new IntAdjacencyGraph.EdgeBuilder(elementsCount, aIndices, bIndices); const id = new Int32Array(builder.slotCount); const magnitude = new Float32Array(builder.slotCount); const distance = new Float32Array(builder.slotCount); for (let i = 0, _i = builder.edgeCount; i < _i; i++) { builder.addNextEdge(); builder.assignProperty(id, ids[i]); builder.assignProperty(magnitude, magnitudes[i]); builder.assignProperty(distance, distances[i]); } return builder.createGraph({ id, magnitude, distance }); } }; } function parseValidationReportXml(xml, model) { const rsrz = new Map(); const rscc = new Map(); const rci = new Map(); const geometryIssues = new Map(); const bondOutliers = { index: new Map(), data: [] }; const angleOutliers = { index: new Map(), data: [] }; const clashesBuilder = ClashesBuilder(model.atomicHierarchy.atoms._rowCount); const { index } = model.atomicHierarchy; const entries = xml.getElementsByTagName('Entry'); if (entries.length === 1) { const chemicalShiftLists = entries[0].getElementsByTagName('chemical_shift_list'); if (chemicalShiftLists.length === 1) { const randomCoilIndices = chemicalShiftLists[0].getElementsByTagName('random_coil_index'); for (let j = 0, jl = randomCoilIndices.length; j < jl; ++j) { const { attributes } = randomCoilIndices[j]; const value = parseFloat(getItem(attributes, 'value')); const auth_asym_id = getItem(attributes, 'chain'); const auth_comp_id = getItem(attributes, 'rescode'); const auth_seq_id = parseInt(getItem(attributes, 'resnum')); const rI = index.findResidueAuth({ auth_asym_id, auth_comp_id, auth_seq_id }); if (rI !== -1) rci.set(rI, value); } } } const groups = xml.getElementsByTagName('ModelledSubgroup'); for (let i = 0, il = groups.length; i < il; ++i) { const g = groups[i]; const ga = g.attributes; const pdbx_PDB_model_num = parseInt(getItem(ga, 'model')); if (model.modelNum !== pdbx_PDB_model_num) continue; const auth_asym_id = getItem(ga, 'chain'); const auth_comp_id = getItem(ga, 'resname'); const auth_seq_id = parseInt(getItem(ga, 'resnum')); const pdbx_PDB_ins_code = getItem(ga, 'icode').trim() || undefined; const label_alt_id = getItem(ga, 'altcode').trim() || undefined; const rI = index.findResidueAuth({ auth_asym_id, auth_comp_id, auth_seq_id, pdbx_PDB_ins_code }); // continue if no residue index is found if (rI === -1) continue; if (ga.getNamedItem('rsrz') !== null) rsrz.set(rI, parseFloat(getItem(ga, 'rsrz'))); if (ga.getNamedItem('rscc') !== null) rscc.set(rI, parseFloat(getItem(ga, 'rscc'))); const isPolymer = getItem(ga, 'seq') !== '.'; const issues = new Set(); if (isPolymer) { const molBondOutliers = g.getElementsByTagName('bond-outlier'); if (molBondOutliers.length) issues.add('bond-outlier'); for (let j = 0, jl = molBondOutliers.length; j < jl; ++j) { const bo = molBondOutliers[j].attributes; const idx = bondOutliers.data.length; const atomA = index.findAtomOnResidue(rI, getItem(bo, 'atom0')); const atomB = index.findAtomOnResidue(rI, getItem(bo, 'atom1')); addIndex(idx, atomA, bondOutliers.index); addIndex(idx, atomB, bondOutliers.index); bondOutliers.data.push({ tag: 'bond-outlier', atomA, atomB, ...getMolInfo(bo) }); } const molAngleOutliers = g.getElementsByTagName('angle-outlier'); if (molAngleOutliers.length) issues.add('angle-outlier'); for (let j = 0, jl = molAngleOutliers.length; j < jl; ++j) { const ao = molAngleOutliers[j].attributes; const idx = bondOutliers.data.length; const atomA = index.findAtomOnResidue(rI, getItem(ao, 'atom0')); const atomB = index.findAtomOnResidue(rI, getItem(ao, 'atom1')); const atomC = index.findAtomOnResidue(rI, getItem(ao, 'atom2')); addIndex(idx, atomA, angleOutliers.index); addIndex(idx, atomB, angleOutliers.index); addIndex(idx, atomC, angleOutliers.index); angleOutliers.data.push({ tag: 'angle-outlier', atomA, atomB, atomC, ...getMolInfo(ao) }); } const planeOutliers = g.getElementsByTagName('plane-outlier'); if (planeOutliers.length) issues.add('plane-outlier'); if (hasAttr(ga, 'rota', 'OUTLIER')) issues.add('rotamer-outlier'); if (hasAttr(ga, 'rama', 'OUTLIER')) issues.add('ramachandran-outlier'); if (hasAttr(ga, 'RNApucker', 'outlier')) issues.add('RNApucker-outlier'); } else { const mogBondOutliers = g.getElementsByTagName('mog-bond-outlier'); if (mogBondOutliers.length) issues.add('mog-bond-outlier'); for (let j = 0, jl = mogBondOutliers.length; j < jl; ++j) { const mbo = mogBondOutliers[j].attributes; const atoms = getItem(mbo, 'atoms').split(','); const idx = bondOutliers.data.length; const atomA = index.findAtomOnResidue(rI, atoms[0]); const atomB = index.findAtomOnResidue(rI, atoms[1]); addIndex(idx, atomA, bondOutliers.index); addIndex(idx, atomB, bondOutliers.index); bondOutliers.data.push({ tag: 'mog-bond-outlier', atomA, atomB, ...getMogInfo(mbo) }); } const mogAngleOutliers = g.getElementsByTagName('mog-angle-outlier'); if (mogAngleOutliers.length) issues.add('mog-angle-outlier'); for (let j = 0, jl = mogAngleOutliers.length; j < jl; ++j) { const mao = mogAngleOutliers[j].attributes; const atoms = getItem(mao, 'atoms').split(','); const idx = angleOutliers.data.length; const atomA = index.findAtomOnResidue(rI, atoms[0]); const atomB = index.findAtomOnResidue(rI, atoms[1]); const atomC = index.findAtomOnResidue(rI, atoms[2]); addIndex(idx, atomA, angleOutliers.index); addIndex(idx, atomB, angleOutliers.index); addIndex(idx, atomC, angleOutliers.index); angleOutliers.data.push({ tag: 'mog-angle-outlier', atomA, atomB, atomC, ...getMogInfo(mao) }); } } const clashes = g.getElementsByTagName('clash'); if (clashes.length) issues.add('clash'); for (let j = 0, jl = clashes.length; j < jl; ++j) { const ca = clashes[j].attributes; const id = parseInt(getItem(ca, 'cid')); const magnitude = parseFloat(getItem(ca, 'clashmag')); const distance = parseFloat(getItem(ca, 'dist')); const label_atom_id = getItem(ca, 'atom'); const element = index.findAtomOnResidue(rI, label_atom_id, label_alt_id); if (element !== -1) { clashesBuilder.add(element, id, magnitude, distance, false); } } const symmClashes = g.getElementsByTagName('symm-clash'); if (symmClashes.length) issues.add('symm-clash'); for (let j = 0, jl = symmClashes.length; j < jl; ++j) { const sca = symmClashes[j].attributes; const id = parseInt(getItem(sca, 'scid')); const magnitude = parseFloat(getItem(sca, 'clashmag')); const distance = parseFloat(getItem(sca, 'dist')); const label_atom_id = getItem(sca, 'atom'); const element = index.findAtomOnResidue(rI, label_atom_id, label_alt_id); if (element !== -1) { clashesBuilder.add(element, id, magnitude, distance, true); } } geometryIssues.set(rI, issues); } const clashes = clashesBuilder.get(); const validationReport = { rsrz, rscc, rci, geometryIssues, bondOutliers, angleOutliers, clashes }; return validationReport; }