diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bf08469f5807e5b28af6fdcdde9fa2879aace4c..1f834fc289fdc36416e4d5d46d0479bf6a2ad8ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,20 @@ Note that since we don't clearly distinguish between a public and private interf - Enable temporal multi-sampling by default - Fix flickering during marking with camera at rest - Enable ``aromaticBonds`` in structure representations by default +- Add ``PluginConfig.Structure.DefaultRepresentationPreset`` +- Add ModelArchive support + - schema extensions (e.g., AlphaFold uses it for the pLDDT score) + - ModelArchive option in DownloadStructure action + - ``model-archive`` GET parameter for Viewer app + - ``Viewer.loadModelArchive`` method +- Improve support for loading AlphaFold structures + - Automatic coloring by pLDDT + - AlphaFold DB option in DownloadStructure action + - ``afdb`` GET parameter for Viewer app + - ``Viewer.loadAlphaFoldDb`` method +- Add QualityAssessment extension (using data from ma_qa_metric_local mmcif category) + - pLDDT & qmean score: coloring, repr presets, molql symbol, loci labels (including avg for mutli-residue selections) + - pLDDT: selection query ## [v3.0.0-dev.5] - 2021-12-16 diff --git a/data/cif-field-names/mmcif-field-names.csv b/data/cif-field-names/mmcif-field-names.csv index 825f2b931474b08d233bd113c7c5854f65b58d20..add635b14b6eb41a87392180d37f885f59bf3fd0 100644 --- a/data/cif-field-names/mmcif-field-names.csv +++ b/data/cif-field-names/mmcif-field-names.csv @@ -246,6 +246,14 @@ citation_author.ordinal exptl.entry_id exptl.method +software.classification +software.date +software.description +software.name +software.pdbx_ordinal +software.type +software.version + struct.entry_id struct.title struct.pdbx_descriptor @@ -802,4 +810,58 @@ ihm_multi_state_modeling.population_fraction_sd ihm_multi_state_modeling.state_type ihm_multi_state_modeling.state_name ihm_multi_state_modeling.experiment_type -ihm_multi_state_modeling.details \ No newline at end of file +ihm_multi_state_modeling.details + +ma_data.content_type +ma_data.content_type_other_details +ma_data.id +ma_data.name + +ma_model_list.data_id +ma_model_list.model_group_id +ma_model_list.model_group_name +ma_model_list.model_id +ma_model_list.model_name +ma_model_list.model_type +ma_model_list.ordinal_id + +ma_qa_metric.id +ma_qa_metric.mode +ma_qa_metric.name +ma_qa_metric.software_group_id +ma_qa_metric.type + +ma_qa_metric_global.metric_id +ma_qa_metric_global.metric_value +ma_qa_metric_global.model_id +ma_qa_metric_global.ordinal_id + +ma_qa_metric_local.label_asym_id +ma_qa_metric_local.label_comp_id +ma_qa_metric_local.label_seq_id +ma_qa_metric_local.metric_id +ma_qa_metric_local.metric_value +ma_qa_metric_local.model_id +ma_qa_metric_local.ordinal_id + +ma_software_group.group_id +ma_software_group.ordinal_id +ma_software_group.software_id + +ma_target_entity.data_id +ma_target_entity.entity_id +ma_target_entity.origin + +ma_target_entity_instance.asym_id +ma_target_entity_instance.details +ma_target_entity_instance.entity_id + +ma_target_ref_db_details.db_accession +ma_target_ref_db_details.db_code +ma_target_ref_db_details.db_name +ma_target_ref_db_details.ncbi_taxonomy_id +ma_target_ref_db_details.organism_scientific +ma_target_ref_db_details.seq_db_align_begin +ma_target_ref_db_details.seq_db_align_end +ma_target_ref_db_details.seq_db_isoform +ma_target_ref_db_details.target_entity_id \ No newline at end of file diff --git a/src/apps/viewer/index.html b/src/apps/viewer/index.html index 90808052807b6f987b53a46e6de5d7af670d4f72..a29689b28e4a4ef6e3335ce2b905272a48c84fc7 100644 --- a/src/apps/viewer/index.html +++ b/src/apps/viewer/index.html @@ -90,6 +90,12 @@ var emdb = getParam('emdb', '[^&]+').trim(); if (emdb) viewer.loadEmdb(emdb); + + var afdb = getParam('afdb', '[^&]+').trim(); + if (afdb) viewer.loadAlphaFoldDb(afdb); + + var modelArchive = getParam('model-archive', '[^&]+').trim(); + if (modelArchive) viewer.loadModelArchive(modelArchive); </script> <!-- __MOLSTAR_ANALYTICS__ --> </body> diff --git a/src/apps/viewer/index.ts b/src/apps/viewer/index.ts index 063c33be45e82ea39c6c3f3a76019e40badef4b1..0ec98c510783e5eccb62b309a8a0d5f5e10072f6 100644 --- a/src/apps/viewer/index.ts +++ b/src/apps/viewer/index.ts @@ -10,13 +10,16 @@ import { CellPack } from '../../extensions/cellpack'; import { DnatcoConfalPyramids } from '../../extensions/dnatco'; import { G3DFormat, G3dProvider } from '../../extensions/g3d/format'; import { GeometryExport } from '../../extensions/geo-export'; +import { MAQualityAssessment } from '../../extensions/model-archive/quality-assessment/behavior'; +import { QualityAssessmentPLDDTPreset, QualityAssessmentQmeanPreset } from '../../extensions/model-archive/quality-assessment/behavior'; +import { QualityAssessment } from '../../extensions/model-archive/quality-assessment/prop'; import { Mp4Export } from '../../extensions/mp4-export'; import { PDBeStructureQualityReport } from '../../extensions/pdbe'; import { RCSBAssemblySymmetry, RCSBValidationReport } from '../../extensions/rcsb'; import { DownloadStructure, PdbDownloadProvider } from '../../mol-plugin-state/actions/structure'; import { DownloadDensity } from '../../mol-plugin-state/actions/volume'; import { PresetTrajectoryHierarchy } from '../../mol-plugin-state/builder/structure/hierarchy-preset'; -import { StructureRepresentationPresetProvider } from '../../mol-plugin-state/builder/structure/representation-preset'; +import { PresetStructureRepresentations, StructureRepresentationPresetProvider } from '../../mol-plugin-state/builder/structure/representation-preset'; import { DataFormatProvider } from '../../mol-plugin-state/formats/provider'; import { BuildInStructureFormat } from '../../mol-plugin-state/formats/structure'; import { BuiltInTrajectoryFormat } from '../../mol-plugin-state/formats/trajectory'; @@ -33,7 +36,7 @@ import { PluginConfig } from '../../mol-plugin/config'; import { PluginLayoutControlsDisplay } from '../../mol-plugin/layout'; import { PluginSpec } from '../../mol-plugin/spec'; import { PluginState } from '../../mol-plugin/state'; -import { StateObjectSelector } from '../../mol-state'; +import { StateObjectRef, StateObjectSelector } from '../../mol-state'; import { Asset } from '../../mol-util/assets'; import { Color } from '../../mol-util/color'; import '../../mol-util/polyfill'; @@ -60,7 +63,8 @@ const Extensions = { 'anvil-membrane-orientation': PluginSpec.Behavior(ANVILMembraneOrientation), 'g3d': PluginSpec.Behavior(G3DFormat), 'mp4-export': PluginSpec.Behavior(Mp4Export), - 'geo-export': PluginSpec.Behavior(GeometryExport) + 'geo-export': PluginSpec.Behavior(GeometryExport), + 'ma-quality-assessment': PluginSpec.Behavior(MAQualityAssessment), }; const DefaultViewerOptions = { @@ -149,7 +153,8 @@ export class Viewer { [PluginConfig.VolumeStreaming.DefaultServer, o.volumeStreamingServer], [PluginConfig.VolumeStreaming.Enabled, !o.volumeStreamingDisabled], [PluginConfig.Download.DefaultPdbProvider, o.pdbProvider], - [PluginConfig.Download.DefaultEmdbProvider, o.emdbProvider] + [PluginConfig.Download.DefaultEmdbProvider, o.emdbProvider], + [PluginConfig.Structure.DefaultRepresentationPreset, ViewerAutoPreset.id], ] }; @@ -158,6 +163,8 @@ export class Viewer { : elementOrId; if (!element) throw new Error(`Could not get element with id '${elementOrId}'`); this.plugin = createPlugin(element, spec); + + this.plugin.builders.structure.representation.registerPreset(ViewerAutoPreset); } setRemoteSnapshot(id: string) { @@ -251,6 +258,35 @@ export class Viewer { })); } + loadAlphaFoldDb(afdb: string) { + const params = DownloadStructure.createDefaultParams(this.plugin.state.data.root.obj!, this.plugin); + return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, { + source: { + name: 'alphafolddb' as const, + params: { + id: afdb, + options: { + ...params.source.params.options, + representation: 'preset-structure-representation-ma-quality-assessment-plddt' + }, + } + } + })); + } + + loadModelArchive(id: string) { + const params = DownloadStructure.createDefaultParams(this.plugin.state.data.root.obj!, this.plugin); + return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, { + source: { + name: 'modelarchive' as const, + params: { + id, + options: params.source.params.options, + } + } + })); + } + /** * @example Load X-ray density from volume server viewer.loadVolumeFromUrl({ @@ -409,4 +445,32 @@ export interface LoadTrajectoryParams { | { kind: 'coordinates-data', data: string | number[] | ArrayBuffer | Uint8Array, format: BuildInStructureFormat }, coordinatesLabel?: string, preset?: keyof PresetTrajectoryHierarchy -} \ No newline at end of file +} + +export const ViewerAutoPreset = StructureRepresentationPresetProvider({ + id: 'preset-structure-representation-viewer-auto', + display: { + name: 'Automatic (w/ Annotation)', group: 'Annotation', + description: 'Show standard automatic representation but colored by quality assessment (if available in the model).' + }, + isApplicable(a) { + return ( + !!a.data.models.some(m => QualityAssessment.isApplicable(m, 'pLDDT')) || + !!a.data.models.some(m => QualityAssessment.isApplicable(m, 'qmean')) + ); + }, + params: () => StructureRepresentationPresetProvider.CommonParams, + async apply(ref, params, plugin) { + const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref); + const structure = structureCell?.obj?.data; + if (!structureCell || !structure) return {}; + + if (!!structure.models.some(m => QualityAssessment.isApplicable(m, 'pLDDT'))) { + return await QualityAssessmentPLDDTPreset.apply(ref, params, plugin); + } else if (!!structure.models.some(m => QualityAssessment.isApplicable(m, 'qmean'))) { + return await QualityAssessmentQmeanPreset.apply(ref, params, plugin); + } else { + return await PresetStructureRepresentations.auto.apply(ref, params, plugin); + } + } +}); \ No newline at end of file diff --git a/src/cli/cifschema/index.ts b/src/cli/cifschema/index.ts index 3b8d96a01ce47682762765f3585d4a8256e9040f..b2704cc791779633b3fda0ef79a4146edb43eb6c 100644 --- a/src/cli/cifschema/index.ts +++ b/src/cli/cifschema/index.ts @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * Copyright (c) 2017-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2017-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> */ @@ -35,20 +35,16 @@ async function runGenerateSchemaMmcif(name: string, fieldNamesPath: string, type const ihmDic = await parseCifText(fs.readFileSync(IHM_DIC_PATH, 'utf8')).run(); if (ihmDic.isError) throw ihmDic; - await ensureCarbBranchDicAvailable(); - const carbBranchDic = await parseCifText(fs.readFileSync(CARB_BRANCH_DIC_PATH, 'utf8')).run(); - if (carbBranchDic.isError) throw carbBranchDic; - - await ensureCarbCompDicAvailable(); - const carbCompDic = await parseCifText(fs.readFileSync(CARB_COMP_DIC_PATH, 'utf8')).run(); - if (carbCompDic.isError) throw carbCompDic; + await ensureMaDicAvailable(); + const maDic = await parseCifText(fs.readFileSync(MA_DIC_PATH, 'utf8')).run(); + if (maDic.isError) throw maDic; const mmcifDicVersion = getDicVersion(mmcifDic.result.blocks[0]); const ihmDicVersion = getDicVersion(ihmDic.result.blocks[0]); - const carbDicVersion = 'draft'; - const version = `Dictionary versions: mmCIF ${mmcifDicVersion}, IHM ${ihmDicVersion}, CARB ${carbDicVersion}.`; + const maDicVersion = getDicVersion(maDic.result.blocks[0]); + const version = `Dictionary versions: mmCIF ${mmcifDicVersion}, IHM ${ihmDicVersion}, MA ${maDicVersion}.`; - const frames: CifFrame[] = [...mmcifDic.result.blocks[0].saveFrames, ...ihmDic.result.blocks[0].saveFrames, ...carbBranchDic.result.blocks[0].saveFrames, ...carbCompDic.result.blocks[0].saveFrames]; + const frames: CifFrame[] = [...mmcifDic.result.blocks[0].saveFrames, ...ihmDic.result.blocks[0].saveFrames, ...maDic.result.blocks[0].saveFrames]; const schema = generateSchema(frames); await runGenerateSchema(name, version, schema, fieldNamesPath, typescript, out, moldbImportPath, addAliases); @@ -139,8 +135,7 @@ async function getFieldNamesFilter(fieldNamesPath: string): Promise<Filter> { async function ensureMmcifDicAvailable() { await ensureDicAvailable(MMCIF_DIC_PATH, MMCIF_DIC_URL); } async function ensureIhmDicAvailable() { await ensureDicAvailable(IHM_DIC_PATH, IHM_DIC_URL); } -async function ensureCarbBranchDicAvailable() { await ensureDicAvailable(CARB_BRANCH_DIC_PATH, CARB_BRANCH_DIC_URL); } -async function ensureCarbCompDicAvailable() { await ensureDicAvailable(CARB_COMP_DIC_PATH, CARB_COMP_DIC_URL); } +async function ensureMaDicAvailable() { await ensureDicAvailable(MA_DIC_PATH, MA_DIC_URL); } async function ensureCifCoreDicAvailable() { await ensureDicAvailable(CIF_CORE_DIC_PATH, CIF_CORE_DIC_URL); await ensureDicAvailable(CIF_CORE_ENUM_PATH, CIF_CORE_ENUM_URL); @@ -165,10 +160,8 @@ const MMCIF_DIC_PATH = `${DIC_DIR}/mmcif_pdbx_v50.dic`; const MMCIF_DIC_URL = 'http://mmcif.wwpdb.org/dictionaries/ascii/mmcif_pdbx_v50.dic'; const IHM_DIC_PATH = `${DIC_DIR}/ihm-extension.dic`; const IHM_DIC_URL = 'https://raw.githubusercontent.com/ihmwg/IHM-dictionary/master/ihm-extension.dic'; -const CARB_BRANCH_DIC_PATH = `${DIC_DIR}/entity_branch-extension.dic`; -const CARB_BRANCH_DIC_URL = 'https://raw.githubusercontent.com/pdbxmmcifwg/carbohydrate-extension/master/dict/entity_branch-extension.dic'; -const CARB_COMP_DIC_PATH = `${DIC_DIR}/chem_comp-extension.dic`; -const CARB_COMP_DIC_URL = 'https://raw.githubusercontent.com/pdbxmmcifwg/carbohydrate-extension/master/dict/chem_comp-extension.dic'; +const MA_DIC_PATH = `${DIC_DIR}/ma-extension.dic`; +const MA_DIC_URL = 'https://raw.githubusercontent.com/ihmwg/MA-dictionary/master/mmcif_ma.dic'; const CIF_CORE_DIC_PATH = `${DIC_DIR}/cif_core.dic`; const CIF_CORE_DIC_URL = 'https://raw.githubusercontent.com/COMCIFS/cif_core/master/cif_core.dic'; diff --git a/src/extensions/model-archive/quality-assessment/behavior.ts b/src/extensions/model-archive/quality-assessment/behavior.ts new file mode 100644 index 0000000000000000000000000000000000000000..ad9c5c0356602b6d51ac4803661e581da68309eb --- /dev/null +++ b/src/extensions/model-archive/quality-assessment/behavior.ts @@ -0,0 +1,212 @@ +/** + * Copyright (c) 2021 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 { PluginBehavior } from '../../../mol-plugin/behavior/behavior'; +import { Loci } from '../../../mol-model/loci'; +import { DefaultQueryRuntimeTable } from '../../../mol-script/runtime/query/compiler'; +import { PLDDTConfidenceColorThemeProvider } from './color/plddt'; +import { QualityAssessment, QualityAssessmentProvider } from './prop'; +import { StructureSelectionCategory, StructureSelectionQuery } from '../../../mol-plugin-state/helpers/structure-selection-query'; +import { MolScriptBuilder as MS } from '../../../mol-script/language/builder'; +import { OrderedSet } from '../../../mol-data/int'; +import { cantorPairing } from '../../../mol-data/util'; +import { QmeanScoreColorThemeProvider } from './color/qmean'; +import { PresetStructureRepresentations, StructureRepresentationPresetProvider } from '../../../mol-plugin-state/builder/structure/representation-preset'; +import { StateObjectRef } from '../../../mol-state'; + +export const MAQualityAssessment = PluginBehavior.create<{ autoAttach: boolean, showTooltip: boolean }>({ + name: 'ma-quality-assessment-prop', + category: 'custom-props', + display: { + name: 'Quality Assessment', + description: 'Data included in Model Archive files.' + }, + ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean, showTooltip: boolean }> { + private provider = QualityAssessmentProvider + + private labelProvider = { + label: (loci: Loci): string | undefined => { + if (!this.params.showTooltip) return; + return [ + plddtLabel(loci), + qmeanLabel(loci), + ].filter(l => !!l).join('</br>'); + } + } + + register(): void { + DefaultQueryRuntimeTable.addCustomProp(this.provider.descriptor); + + this.ctx.customModelProperties.register(this.provider, this.params.autoAttach); + + this.ctx.managers.lociLabels.addProvider(this.labelProvider); + + this.ctx.representation.structure.themes.colorThemeRegistry.add(PLDDTConfidenceColorThemeProvider); + this.ctx.representation.structure.themes.colorThemeRegistry.add(QmeanScoreColorThemeProvider); + + this.ctx.query.structure.registry.add(confidentPLDDT); + + this.ctx.builders.structure.representation.registerPreset(QualityAssessmentPLDDTPreset); + this.ctx.builders.structure.representation.registerPreset(QualityAssessmentQmeanPreset); + } + + update(p: { autoAttach: boolean, showTooltip: boolean }) { + const updated = this.params.autoAttach !== p.autoAttach; + this.params.autoAttach = p.autoAttach; + this.params.showTooltip = p.showTooltip; + this.ctx.customStructureProperties.setDefaultAutoAttach(this.provider.descriptor.name, this.params.autoAttach); + return updated; + } + + unregister() { + DefaultQueryRuntimeTable.removeCustomProp(this.provider.descriptor); + + this.ctx.customStructureProperties.unregister(this.provider.descriptor.name); + + this.ctx.managers.lociLabels.removeProvider(this.labelProvider); + + this.ctx.representation.structure.themes.colorThemeRegistry.remove(PLDDTConfidenceColorThemeProvider); + this.ctx.representation.structure.themes.colorThemeRegistry.remove(QmeanScoreColorThemeProvider); + + this.ctx.query.structure.registry.remove(confidentPLDDT); + + this.ctx.builders.structure.representation.unregisterPreset(QualityAssessmentPLDDTPreset); + this.ctx.builders.structure.representation.unregisterPreset(QualityAssessmentQmeanPreset); + } + }, + params: () => ({ + autoAttach: PD.Boolean(false), + showTooltip: PD.Boolean(true), + }) +}); + +// + +function plddtCategory(score: number) { + if (score > 50 && score <= 70) return 'Low'; + if (score > 70 && score <= 90) return 'Confident'; + if (score > 90) return 'Very high'; + return 'Very low'; +} + +function plddtLabel(loci: Loci): string | undefined { + return metricLabel(loci, 'pLDDT', (scoreAvg: number, countInfo: string) => `pLDDT Score ${countInfo}: ${scoreAvg.toFixed(2)} <small>(${plddtCategory(scoreAvg)})</small>`); +} + +function qmeanLabel(loci: Loci): string | undefined { + return metricLabel(loci, 'qmean', (scoreAvg: number, countInfo: string) => `QMEAN Score ${countInfo}: ${scoreAvg.toFixed(2)}`); +} + +function metricLabel(loci: Loci, name: 'qmean' | 'pLDDT', label: (scoreAvg: number, countInfo: string) => string): string | undefined { + if (loci.kind === 'element-loci') { + if (loci.elements.length === 0) return; + + const seen = new Set<number>(); + const scoreSeen = new Set<number>(); + let scoreSum = 0; + + for (const { indices, unit } of loci.elements) { + const metric = QualityAssessmentProvider.get(unit.model).value?.[name]; + if (!metric) continue; + + const residueIndex = unit.model.atomicHierarchy.residueAtomSegments.index; + const { elements } = unit; + + OrderedSet.forEach(indices, idx => { + const eI = elements[idx]; + const rI = residueIndex[eI]; + + const residueKey = cantorPairing(rI, unit.id); + if (!seen.has(residueKey)) { + const score = metric.get(residueIndex[eI]) ?? -1; + if (score !== -1) { + scoreSum += score; + scoreSeen.add(residueKey); + } + seen.add(residueKey); + } + }); + } + + if (seen.size === 0) return; + + const summary: string[] = []; + + if (scoreSeen.size) { + const countInfo = `<small>(${scoreSeen.size} ${scoreSeen.size > 1 ? 'Residues avg.' : 'Residue'})</small>`; + const scoreAvg = scoreSum / scoreSeen.size; + summary.push(label(scoreAvg, countInfo)); + } + + if (summary.length) { + return summary.join('</br>'); + } + } +} + +// + +const confidentPLDDT = StructureSelectionQuery('Confident pLDDT (> 70)', MS.struct.modifier.union([ + MS.struct.modifier.wholeResidues([ + MS.struct.modifier.union([ + MS.struct.generator.atomGroups({ + 'chain-test': MS.core.rel.eq([MS.ammp('objectPrimitive'), 'atomistic']), + 'residue-test': MS.core.rel.gr([QualityAssessment.symbols.pLDDT.symbol(), 70]), + }) + ]) + ]) +]), { + description: 'Select residues with a pLDDT > 70 (confident).', + category: StructureSelectionCategory.Validation, + ensureCustomProperties: async (ctx, structure) => { + for (const m of structure.models) { + await QualityAssessmentProvider.attach(ctx, m, void 0, true); + } + } +}); + +// + +export const QualityAssessmentPLDDTPreset = StructureRepresentationPresetProvider({ + id: 'preset-structure-representation-ma-quality-assessment-plddt', + display: { + name: 'Quality Assessment (pLDDT)', group: 'Annotation', + description: 'Color structure based on pLDDT Confidence.' + }, + isApplicable(a) { + return !!a.data.models.some(m => QualityAssessment.isApplicable(m, 'pLDDT')); + }, + params: () => StructureRepresentationPresetProvider.CommonParams, + async apply(ref, params, plugin) { + const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref); + const structure = structureCell?.obj?.data; + if (!structureCell || !structure) return {}; + + const colorTheme = PLDDTConfidenceColorThemeProvider.name as any; + return await PresetStructureRepresentations.auto.apply(ref, { ...params, theme: { globalName: colorTheme, focus: { name: colorTheme } } }, plugin); + } +}); + +export const QualityAssessmentQmeanPreset = StructureRepresentationPresetProvider({ + id: 'preset-structure-representation-ma-quality-assessment-qmean', + display: { + name: 'Quality Assessment (QMEAN)', group: 'Annotation', + description: 'Color structure based on QMEAN Score.' + }, + isApplicable(a) { + return !!a.data.models.some(m => QualityAssessment.isApplicable(m, 'qmean')); + }, + params: () => StructureRepresentationPresetProvider.CommonParams, + async apply(ref, params, plugin) { + const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref); + const structure = structureCell?.obj?.data; + if (!structureCell || !structure) return {}; + + const colorTheme = QmeanScoreColorThemeProvider.name as any; + return await PresetStructureRepresentations.auto.apply(ref, { ...params, theme: { globalName: colorTheme, focus: { name: colorTheme } } }, plugin); + } +}); \ No newline at end of file diff --git a/src/extensions/model-archive/quality-assessment/color/plddt.ts b/src/extensions/model-archive/quality-assessment/color/plddt.ts new file mode 100644 index 0000000000000000000000000000000000000000..0e81d3f17d0642c99d121c98917c4c1edcb10efa --- /dev/null +++ b/src/extensions/model-archive/quality-assessment/color/plddt.ts @@ -0,0 +1,106 @@ +/** + * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Mandar Deshpande <mandar@ebi.ac.uk> + * @author Sebastian Bittrich <sebastian.bittrich@rcsb.org> + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { QualityAssessment, QualityAssessmentProvider } from '../prop'; +import { Location } from '../../../../mol-model/location'; +import { Bond, StructureElement, Unit } from '../../../../mol-model/structure'; +import { ColorTheme, LocationColor } from '../../../../mol-theme/color'; +import { ThemeDataContext } from '../../../../mol-theme/theme'; +import { Color } from '../../../../mol-util/color'; +import { ParamDefinition as PD } from '../../../../mol-util/param-definition'; +import { CustomProperty } from '../../../../mol-model-props/common/custom-property'; +import { TableLegend } from '../../../../mol-util/legend'; + +const DefaultColor = Color(0xaaaaaa); +const ConfidenceColors = { + 'No Score': DefaultColor, + 'Very Low': Color(0xff7d45), + 'Low': Color(0xffdb13), + 'Confident': Color(0x65cbf3), + 'Very High': Color(0x0053d6) +}; + +const ConfidenceColorLegend = TableLegend(Object.entries(ConfidenceColors)); + +export function getPLDDTConfidenceColorThemeParams(ctx: ThemeDataContext) { + return {}; +} +export type PLDDTConfidenceColorThemeParams = ReturnType<typeof getPLDDTConfidenceColorThemeParams> + +export function PLDDTConfidenceColorTheme(ctx: ThemeDataContext, props: PD.Values<PLDDTConfidenceColorThemeParams>): ColorTheme<PLDDTConfidenceColorThemeParams> { + let color: LocationColor = () => DefaultColor; + + if (ctx.structure) { + const l = StructureElement.Location.create(ctx.structure.root); + + const getColor = (location: StructureElement.Location): Color => { + const { unit, element } = location; + if (!Unit.isAtomic(unit)) return DefaultColor; + const qualityAssessment = QualityAssessmentProvider.get(unit.model).value; + const score = qualityAssessment?.pLDDT?.get(unit.model.atomicHierarchy.residueAtomSegments.index[element]) ?? -1; + if (score < 0) { + return DefaultColor; + } else if (score <= 50) { + return Color(0xff7d45); + } else if (score <= 70) { + return Color(0xffdb13); + } else if (score <= 90) { + return Color(0x65cbf3); + } else { + return Color(0x0053d6); + } + }; + + color = (location: Location) => { + if (StructureElement.Location.is(location)) { + return getColor(location); + } else if (Bond.isLocation(location)) { + l.unit = location.aUnit; + l.element = location.aUnit.elements[location.aIndex]; + return getColor(l); + } + return DefaultColor; + }; + } + + return { + factory: PLDDTConfidenceColorTheme, + granularity: 'group', + preferSmoothing: true, + color, + props, + description: 'Assigns residue colors according to the pLDDT Confidence score.', + legend: ConfidenceColorLegend + }; +} + +export const PLDDTConfidenceColorThemeProvider: ColorTheme.Provider<PLDDTConfidenceColorThemeParams, 'plddt-confidence'> = { + name: 'plddt-confidence', + label: 'pLDDT Confidence', + category: ColorTheme.Category.Validation, + factory: PLDDTConfidenceColorTheme, + getParams: getPLDDTConfidenceColorThemeParams, + defaultValues: PD.getDefaultValues(getPLDDTConfidenceColorThemeParams({})), + isApplicable: (ctx: ThemeDataContext) => !!ctx.structure?.models.some(m => QualityAssessment.isApplicable(m, 'pLDDT')), + ensureCustomProperties: { + attach: async (ctx: CustomProperty.Context, data: ThemeDataContext) => { + if (data.structure) { + for (const m of data.structure.models) { + await QualityAssessmentProvider.attach(ctx, m, void 0, true); + } + } + }, + detach: async (data: ThemeDataContext) => { + if (data.structure) { + for (const m of data.structure.models) { + QualityAssessmentProvider.ref(m, false); + } + } + } + } +}; \ No newline at end of file diff --git a/src/extensions/model-archive/quality-assessment/color/qmean.ts b/src/extensions/model-archive/quality-assessment/color/qmean.ts new file mode 100644 index 0000000000000000000000000000000000000000..173a7db95121e38be4e3a8e5bdb6857f3a76d87b --- /dev/null +++ b/src/extensions/model-archive/quality-assessment/color/qmean.ts @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { QualityAssessment, QualityAssessmentProvider } from '../prop'; +import { Location } from '../../../../mol-model/location'; +import { Bond, StructureElement, Unit } from '../../../../mol-model/structure'; +import { ColorTheme, LocationColor } from '../../../../mol-theme/color'; +import { ThemeDataContext } from '../../../../mol-theme/theme'; +import { Color, ColorScale } from '../../../../mol-util/color'; +import { ParamDefinition as PD } from '../../../../mol-util/param-definition'; +import { CustomProperty } from '../../../../mol-model-props/common/custom-property'; + +const DefaultColor = Color(0xaaaaaa); + +export function getQmeanScoreColorThemeParams(ctx: ThemeDataContext) { + return {}; +} +export type QmeanScoreColorThemeParams = ReturnType<typeof getQmeanScoreColorThemeParams> + +export function QmeanScoreColorTheme(ctx: ThemeDataContext, props: PD.Values<QmeanScoreColorThemeParams>): ColorTheme<QmeanScoreColorThemeParams> { + let color: LocationColor = () => DefaultColor; + + const scale = ColorScale.create({ + domain: [0, 1], + listOrName: [ + [Color(0xFF5000), 0.5], [Color(0x025AFD), 1.0] + ] + }); + + if (ctx.structure) { + const l = StructureElement.Location.create(ctx.structure.root); + + const getColor = (location: StructureElement.Location): Color => { + const { unit, element } = location; + if (!Unit.isAtomic(unit)) return DefaultColor; + const qualityAssessment = QualityAssessmentProvider.get(unit.model).value; + const score = qualityAssessment?.qmean?.get(unit.model.atomicHierarchy.residueAtomSegments.index[element]) ?? -1; + if (score < 0) { + return DefaultColor; + } else { + return scale.color(score); + } + }; + + color = (location: Location) => { + if (StructureElement.Location.is(location)) { + return getColor(location); + } else if (Bond.isLocation(location)) { + l.unit = location.aUnit; + l.element = location.aUnit.elements[location.aIndex]; + return getColor(l); + } + return DefaultColor; + }; + } + + return { + factory: QmeanScoreColorTheme, + granularity: 'group', + preferSmoothing: true, + color, + props, + description: 'Assigns residue colors according to the QMEAN score.', + legend: scale.legend + }; +} + +export const QmeanScoreColorThemeProvider: ColorTheme.Provider<QmeanScoreColorThemeParams, 'qmean-score'> = { + name: 'qmean-score', + label: 'QMEAN Score', + category: ColorTheme.Category.Validation, + factory: QmeanScoreColorTheme, + getParams: getQmeanScoreColorThemeParams, + defaultValues: PD.getDefaultValues(getQmeanScoreColorThemeParams({})), + isApplicable: (ctx: ThemeDataContext) => !!ctx.structure?.models.some(m => QualityAssessment.isApplicable(m, 'qmean')), + ensureCustomProperties: { + attach: async (ctx: CustomProperty.Context, data: ThemeDataContext) => { + if (data.structure) { + for (const m of data.structure.models) { + await QualityAssessmentProvider.attach(ctx, m, void 0, true); + } + } + }, + detach: async (data: ThemeDataContext) => { + if (data.structure) { + for (const m of data.structure.models) { + QualityAssessmentProvider.ref(m, false); + } + } + } + } +}; \ No newline at end of file diff --git a/src/extensions/model-archive/quality-assessment/prop.ts b/src/extensions/model-archive/quality-assessment/prop.ts new file mode 100644 index 0000000000000000000000000000000000000000..ce8c1a3975a92800bdca1f0a334f899a1767221a --- /dev/null +++ b/src/extensions/model-archive/quality-assessment/prop.ts @@ -0,0 +1,131 @@ +/** + * Copyright (c) 2021 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 { Unit } from '../../../mol-model/structure'; +import { CustomProperty } from '../../../mol-model-props/common/custom-property'; +import { CustomModelProperty } from '../../../mol-model-props/common/custom-model-property'; +import { Model, ResidueIndex } from '../../../mol-model/structure/model'; +import { QuerySymbolRuntime } from '../../../mol-script/runtime/query/compiler'; +import { CustomPropSymbol } from '../../../mol-script/language/symbol'; +import { Type } from '../../../mol-script/language/type'; +import { CustomPropertyDescriptor } from '../../../mol-model/custom-property'; +import { MmcifFormat } from '../../../mol-model-formats/structure/mmcif'; + +export { QualityAssessment }; + +interface QualityAssessment { + localMetrics: Map<string, Map<ResidueIndex, number>> + pLDDT?: Map<ResidueIndex, number> + qmean?: Map<ResidueIndex, number> +} + +namespace QualityAssessment { + const Empty = { + value: { + localMetrics: new Map() + } + }; + + export function isApplicable(model?: Model, localMetricName?: 'pLDDT' | 'qmean'): boolean { + if (!model || !MmcifFormat.is(model.sourceData)) return false; + const { db } = model.sourceData.data; + const hasLocalMetric = ( + db.ma_qa_metric.id.isDefined && + db.ma_qa_metric_local.ordinal_id.isDefined + ); + if (localMetricName && hasLocalMetric) { + for (let i = 0, il = db.ma_qa_metric._rowCount; i < il; i++) { + if (db.ma_qa_metric.mode.value(i) !== 'local') continue; + if (localMetricName === db.ma_qa_metric.name.value(i)) return true; + } + return false; + } else { + return hasLocalMetric; + } + } + + export async function obtain(ctx: CustomProperty.Context, model: Model, props: QualityAssessmentProps): Promise<CustomProperty.Data<QualityAssessment>> { + if (!model || !MmcifFormat.is(model.sourceData)) return Empty; + const { ma_qa_metric, ma_qa_metric_local } = model.sourceData.data.db; + const { model_id, label_asym_id, label_seq_id, metric_id, metric_value } = ma_qa_metric_local; + const { index } = model.atomicHierarchy; + + // for simplicity we assume names in ma_qa_metric for mode 'local' are unique + const localMetrics = new Map<string, Map<ResidueIndex, number>>(); + const localNames = new Map<number, string>(); + + for (let i = 0, il = ma_qa_metric._rowCount; i < il; i++) { + if (ma_qa_metric.mode.value(i) !== 'local') continue; + + const name = ma_qa_metric.name.value(i); + if (localMetrics.has(name)) { + console.warn(`local ma_qa_metric with name '${name}' already added`); + continue; + } + + localMetrics.set(name, new Map()); + localNames.set(ma_qa_metric.id.value(i), name); + } + + for (let i = 0, il = ma_qa_metric_local._rowCount; i < il; i++) { + if (model_id.value(i) !== model.modelNum) continue; + + const labelAsymId = label_asym_id.value(i); + const entityIndex = index.findEntity(labelAsymId); + const rI = index.findResidue(model.entities.data.id.value(entityIndex), labelAsymId, label_seq_id.value(i)); + const name = localNames.get(metric_id.value(i))!; + localMetrics.get(name)!.set(rI, metric_value.value(i)); + } + + return { + value: { + localMetrics, + pLDDT: localMetrics.get('pLDDT'), + qmean: localMetrics.get('qmean'), + } + }; + } + + export const symbols = { + pLDDT: QuerySymbolRuntime.Dynamic(CustomPropSymbol('ma', 'quality-assessment.pLDDT', Type.Num), + ctx => { + const { unit, element } = ctx.element; + if (!Unit.isAtomic(unit)) return -1; + const qualityAssessment = QualityAssessmentProvider.get(unit.model).value; + return qualityAssessment?.pLDDT?.get(unit.model.atomicHierarchy.residueAtomSegments.index[element]) ?? -1; + } + ), + qmean: QuerySymbolRuntime.Dynamic(CustomPropSymbol('ma', 'quality-assessment.qmean', Type.Num), + ctx => { + const { unit, element } = ctx.element; + if (!Unit.isAtomic(unit)) return -1; + const qualityAssessment = QualityAssessmentProvider.get(unit.model).value; + return qualityAssessment?.qmean?.get(unit.model.atomicHierarchy.residueAtomSegments.index[element]) ?? -1; + } + ), + }; +} + +export const QualityAssessmentParams = { }; +export type QualityAssessmentParams = typeof QualityAssessmentParams +export type QualityAssessmentProps = PD.Values<QualityAssessmentParams> + +export const QualityAssessmentProvider: CustomModelProperty.Provider<QualityAssessmentParams, QualityAssessment> = CustomModelProperty.createProvider({ + label: 'QualityAssessment', + descriptor: CustomPropertyDescriptor({ + name: 'ma_quality_assessment', + symbols: QualityAssessment.symbols + }), + type: 'static', + defaultParams: QualityAssessmentParams, + getParams: (data: Model) => QualityAssessmentParams, + isApplicable: (data: Model) => QualityAssessment.isApplicable(data), + obtain: async (ctx: CustomProperty.Context, data: Model, props: Partial<QualityAssessmentProps>) => { + const p = { ...PD.getDefaultValues(QualityAssessmentParams), ...props }; + return await QualityAssessment.obtain(ctx, data, p); + } +}); \ No newline at end of file diff --git a/src/mol-io/reader/cif/schema/mmcif.ts b/src/mol-io/reader/cif/schema/mmcif.ts index 0a0455bf5c945e04a5b61f239dbb0a086d3f0c13..fc8dfc233c1100f66cdd96461cd2d818c60274b8 100644 --- a/src/mol-io/reader/cif/schema/mmcif.ts +++ b/src/mol-io/reader/cif/schema/mmcif.ts @@ -1,7 +1,7 @@ /** * Copyright (c) 2017-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. * - * Code-generated 'mmCIF' schema file. Dictionary versions: mmCIF 5.352, IHM 1.17, CARB draft. + * Code-generated 'mmCIF' schema file. Dictionary versions: mmCIF 5.353, IHM 1.17, MA 1.3.3. * * @author molstar/ciftools package */ @@ -942,6 +942,48 @@ export const mmCIF_Schema = { */ method: Aliased<'X-RAY DIFFRACTION' | 'NEUTRON DIFFRACTION' | 'FIBER DIFFRACTION' | 'ELECTRON CRYSTALLOGRAPHY' | 'ELECTRON MICROSCOPY' | 'SOLUTION NMR' | 'SOLID-STATE NMR' | 'SOLUTION SCATTERING' | 'POWDER DIFFRACTION' | 'INFRARED SPECTROSCOPY' | 'EPR' | 'FLUORESCENCE TRANSFER' | 'THEORETICAL MODEL'>(str), }, + /** + * Data items in the SOFTWARE category record details about + * the software used in the structure analysis, which implies + * any software used in the generation of any data items + * associated with the structure determination and + * structure representation. + * + * These data items allow computer programs to be referenced + * in more detail than data items in the COMPUTING category do. + */ + software: { + /** + * The classification of the program according to its + * major function. + */ + classification: str, + /** + * The date the software was released. + */ + date: str, + /** + * Description of the software. + */ + description: str, + /** + * The name of the software. + */ + name: str, + /** + * The classification of the software according to the most + * common types. + */ + type: Aliased<'program' | 'library' | 'package' | 'filter' | 'jiffy' | 'other'>(str), + /** + * The version of the software. + */ + version: str, + /** + * An ordinal index for this category + */ + pdbx_ordinal: int, + }, /** * Data items in the STRUCT category record details about the * description of the crystallographic structure. @@ -4717,6 +4759,289 @@ export const mmCIF_Schema = { */ dataset_list_id: int, }, + /** + * Data items in the MA_MODEL_LIST category record the + * details of the models being deposited. + */ + ma_model_list: { + /** + * A unique identifier for the model / model group combination. + */ + ordinal_id: int, + /** + * A unique identifier for the structural model being deposited. + */ + model_id: int, + /** + * An identifier to group structural models into collections or sets. + * A cluster of models and its representative can either be grouped together + * or can be separate groups in the ma_model_list table. The choice between + * the two options should be decided based on how the modeling was carried out + * and how the representative was chosen. If the representative is a member of + * the ensemble (i.e., best scoring model), then it is recommended that the + * representative and the ensemble belong to the same model group. If the + * representative is calculated from the ensemble (i.e., centroid), then it is + * recommended that the representative be separated into a different group. + * If the models do not need to be grouped into collections, then the + * _ma_model_list.model_group_id is the same as _ma_model_list.model_id. + */ + model_group_id: int, + /** + * A decsriptive name for the model. + */ + model_name: str, + /** + * A decsriptive name for the model group. + */ + model_group_name: str, + /** + * The type of model. + */ + model_type: Aliased<'Homology model' | 'Ab initio model' | 'Other'>(str), + /** + * The data_id identifier. This data item is a pointer to + * _ma_data.id in the MA_DATA category. + */ + data_id: int, + }, + /** + * Data items in the MA_TARGET_ENTITY category record details about + * the target entities. The details are provided for each entity + * being modeled. + */ + ma_target_entity: { + /** + * A unique identifier for the distinct molecular entity of the target. + * This data item is a pointer to _entity.id in the ENTITY category. + */ + entity_id: str, + /** + * The data_id identifier. This data item is a pointer to + * _ma_data.id in the MA_DATA category. + */ + data_id: int, + /** + * The origin of the target entity. + */ + origin: Aliased<'reference database' | 'designed'>(str), + }, + /** + * Data items in the MA_TARGET_ENTITY_INSTANCE category record details about + * the instances of target entities modeled. + */ + ma_target_entity_instance: { + /** + * A unique identifier for the instance of the entity. + */ + asym_id: str, + /** + * A unique identifier for the distinct molecular entity of the target. + * This data item is a pointer to _ma_target_entity.entity_id in the + * MA_TARGET_ENTITY category. + */ + entity_id: str, + /** + * Additional details about the entity instance. + */ + details: str, + }, + /** + * Data items in the MA_TARGET_REF_DB_DETAILS category record details about + * the reference databases for the target sequences. + */ + ma_target_ref_db_details: { + /** + * An identifier for the target entity. + */ + target_entity_id: str, + /** + * The name of the database containing reference information about + * this entity or biological unit. + */ + db_name: Aliased<'UNP' | 'GB' | 'OrthoDB' | 'NCBI' | 'JGI' | 'Other'>(str), + /** + * The code for this entity or biological unit or for a closely + * related entity or biological unit in the named database. + * This can include the version number. + */ + db_code: str, + /** + * Accession code assigned by the reference database. + */ + db_accession: str, + /** + * Database code assigned by the reference database for a sequence isoform. An isoform sequence is an + * alternative protein sequence that can be generated from the same gene by a single or by a combination of + * biological events such as: alternative promoter usage, alternative splicing, alternative initiation + * and ribosomal frameshifting. + */ + seq_db_isoform: str, + /** + * Beginning index in the chemical sequence from the + * reference database. + */ + seq_db_align_begin: str, + /** + * Ending index in the chemical sequence from the + * reference database. + */ + seq_db_align_end: str, + /** + * Taxonomy identifier provided by NCBI. + */ + ncbi_taxonomy_id: str, + /** + * Scientific name of the organism. + */ + organism_scientific: str, + }, + /** + * Data items in the MA_DATA category capture the different kinds of + * data used in the modeling. These can be multiple sequence + * alignments, spatial restraints, template structures etc. + */ + ma_data: { + /** + * A unique identifier for the data. + */ + id: int, + /** + * The type of data held in the dataset. + */ + content_type: Aliased<'target' | 'template structure' | 'polymeric template library' | 'spatial restraints' | 'target-template alignment' | 'coevolution MSA' | 'model coordinates' | 'other'>(str), + /** + * Details for other content types. + */ + content_type_other_details: str, + /** + * An author-given name for the content held in the dataset. + */ + name: str, + }, + /** + * Data items in the MA_SOFTWARE_GROUP category describes the + * collection of software into groups so that they can be used + * efficiently in the MA_PROTOCOL_STEP category. + */ + ma_software_group: { + /** + * A unique identifier for the category. + */ + ordinal_id: int, + /** + * An identifier for the group entry. + * If data does not need to be grouped, then _ma_software_group.group_id + * is the same as _ma_software_group.software_id. + */ + group_id: int, + /** + * The identifier for the software. + * This data item is a pointer to _software.pdbx_ordinal + * in the SOFTWARE category. + */ + software_id: int, + }, + /** + * Data items in the MA_QA_METRIC category record the + * details of the metrics use to assess model quality. + */ + ma_qa_metric: { + /** + * An identifier for the QA metric. + */ + id: int, + /** + * Name of the QA metric. + */ + name: str, + /** + * The type of QA metric. + */ + type: Aliased<'zscore' | 'energy' | 'distance' | 'normalized score' | 'pLDDT' | 'PAE' | 'contact probability' | 'other'>(str), + /** + * The mode of calculation of the QA metric. + */ + mode: Aliased<'local' | 'global' | 'local-pairwise'>(str), + /** + * Identifier to the set of software used to calculate the QA metric. + * This data item is a pointer to the _ma_software_group.group_id in the + * MA_SOFTWARE_GROUP category. + */ + software_group_id: int, + }, + /** + * Data items in the MA_QA_METRIC_GLOBAL category captures the + * details of the global QA metrics, calculated at the model-level. + */ + ma_qa_metric_global: { + /** + * A unique identifier for the category. + */ + ordinal_id: int, + /** + * The identifier for the structural model, for which global QA metric is provided. + * This data item is a pointer to _ma_model_list.model_id + * in the MA_MODEL_LIST category. + */ + model_id: int, + /** + * The identifier for the QA metric. + * This data item is a pointer to _ma_qa_metric.id in the + * MA_QA_METRIC category. + */ + metric_id: int, + /** + * The value of the global QA metric. + */ + metric_value: float, + }, + /** + * Data items in the MA_QA_METRIC_LOCAL category captures the + * details of the local QA metrics, calculated at the residue-level. + */ + ma_qa_metric_local: { + /** + * A unique identifier for the category. + */ + ordinal_id: int, + /** + * The identifier for the structural model, for which local QA metric is provided. + * This data item is a pointer to _ma_model_list.model_id + * in the MA_MODEL_LIST category. + */ + model_id: int, + /** + * The identifier for the asym id of the residue in the + * structural model, for which local QA metric is provided. + * This data item is a pointer to _atom_site.label_asym_id + * in the ATOM_SITE category. + */ + label_asym_id: str, + /** + * The identifier for the sequence index of the residue + * in the structural model, for which local QA metric is provided. + * This data item is a pointer to _atom_site.label_seq_id + * in the ATOM_SITE category. + */ + label_seq_id: int, + /** + * The component identifier for the residue in the + * structural model, for which local QA metric is provided. + * This data item is a pointer to _atom_site.label_comp_id + * in the ATOM_SITE category. + */ + label_comp_id: str, + /** + * The identifier for the QA metric. + * This data item is a pointer to _ma_qa_metric.id in the + * MA_QA_METRIC category. + */ + metric_id: int, + /** + * The value of the local QA metric. + */ + metric_value: float, + }, }; export type mmCIF_Schema = typeof mmCIF_Schema; diff --git a/src/mol-plugin-state/actions/structure.ts b/src/mol-plugin-state/actions/structure.ts index 0239487298f996c4598e7ef0e280cb1680327573..21e098b3e1411e03634d1bf4dd38519be9272be9 100644 --- a/src/mol-plugin-state/actions/structure.ts +++ b/src/mol-plugin-state/actions/structure.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal <david.sehnal@gmail.com> * @author Alexander Rose <alexander.rose@weirdbyte.de> @@ -18,19 +18,23 @@ import { Download } from '../transforms/data'; import { CustomModelProperties, CustomStructureProperties, TrajectoryFromModelAndCoordinates } from '../transforms/model'; import { Asset } from '../../mol-util/assets'; import { PluginConfig } from '../../mol-plugin/config'; +import { getFileInfo } from '../../mol-util/file-info'; -const DownloadModelRepresentationOptions = (plugin: PluginContext) => PD.Group({ - type: RootStructureDefinition.getParams(void 0, 'auto').type, - representation: PD.Select(PresetStructureRepresentations.auto.id, - plugin.builders.structure.representation.getPresets().map(p => [p.id, p.display.name, p.display.group] as any), - { description: 'Which representation preset to use.' }), - representationParams: PD.Group(StructureRepresentationPresetProvider.CommonParams, { isHidden: true }), - asTrajectory: PD.Optional(PD.Boolean(false, { description: 'Load all entries into a single trajectory.' })) -}, { isExpanded: false }); +const DownloadModelRepresentationOptions = (plugin: PluginContext) => { + const representationDefault = plugin.config.get(PluginConfig.Structure.DefaultRepresentationPreset) || PresetStructureRepresentations.auto.id; + return PD.Group({ + type: RootStructureDefinition.getParams(void 0, 'auto').type, + representation: PD.Select(representationDefault, + plugin.builders.structure.representation.getPresets().map(p => [p.id, p.display.name, p.display.group] as any), + { description: 'Which representation preset to use.' }), + representationParams: PD.Group(StructureRepresentationPresetProvider.CommonParams, { isHidden: true }), + asTrajectory: PD.Optional(PD.Boolean(false, { description: 'Load all entries into a single trajectory.' })) + }, { isExpanded: false }); +}; export const PdbDownloadProvider = { 'rcsb': PD.Group({ - encoding: PD.Select('bcif', [['cif', 'cif'], ['bcif', 'bcif']] as ['cif' | 'bcif', string][]), + encoding: PD.Select('bcif', PD.arrayToOptions(['cif', 'bcif'] as const)), }, { label: 'RCSB PDB', isFlat: true }), 'pdbe': PD.Group({ variant: PD.Select('updated-bcif', [['updated-bcif', 'Updated (bcif)'], ['updated', 'Updated'], ['archival', 'Archival']] as ['updated' | 'updtaed-bcif' | 'archival', string][]), @@ -58,7 +62,7 @@ const DownloadStructure = StateAction.build({ 'pdb-dev': PD.Group({ provider: PD.Group({ id: PD.Text('PDBDEV_00000001', { label: 'PDBDev Id(s)', description: 'One or more comma/space separated ids.' }), - encoding: PD.Select('bcif', [['cif', 'cif'], ['bcif', 'bcif']] as ['cif' | 'bcif', string][]), + encoding: PD.Select('bcif', PD.arrayToOptions(['cif', 'bcif'] as const)), }, { pivot: 'id' }), options }, { isFlat: true, label: 'PDBDEV' }), @@ -66,6 +70,14 @@ const DownloadStructure = StateAction.build({ id: PD.Text('Q9Y2I8', { label: 'UniProtKB AC(s)', description: 'One or more comma/space separated ACs.' }), options }, { isFlat: true, label: 'SWISS-MODEL', description: 'Loads the best homology model or experimental structure' }), + 'alphafolddb': PD.Group({ + id: PD.Text('Q8W3K0', { label: 'UniProtKB AC(s)', description: 'One or more comma/space separated ACs.' }), + options + }, { isFlat: true, label: 'AlphaFold DB', description: 'Loads the predicted model if available' }), + 'modelarchive': PD.Group({ + id: PD.Text('ma-bak-cepc-0003', { label: 'Accession Code(s)', description: 'One or more comma/space separated ACs.' }), + options + }, { isFlat: true, label: 'Model Archive' }), 'pubchem': PD.Group({ id: PD.Text('2244,2245', { label: 'PubChem ID', description: 'One or more comma/space separated IDs.' }), options @@ -84,7 +96,7 @@ const DownloadStructure = StateAction.build({ const src = params.source; let downloadParams: StateTransformer.Params<Download>[]; - let asTrajectory = false, format: BuiltInTrajectoryFormat = 'mmcif'; + let asTrajectory = false, format: BuiltInTrajectoryFormat | 'auto' = 'mmcif'; switch (src.name) { case 'url': @@ -92,7 +104,7 @@ const DownloadStructure = StateAction.build({ format = src.params.format; break; case 'pdb': - downloadParams = src.params.provider.server.name === 'pdbe' + downloadParams = await (src.params.provider.server.name === 'pdbe' ? src.params.provider.server.params.variant === 'updated' ? getDownloadParams(src.params.provider.id, id => `https://www.ebi.ac.uk/pdbe/static/entry/${id.toLowerCase()}_updated.cif`, id => `PDBe: ${id} (updated cif)`, false) : src.params.provider.server.params.variant === 'updated-bcif' @@ -100,11 +112,12 @@ const DownloadStructure = StateAction.build({ : getDownloadParams(src.params.provider.id, id => `https://www.ebi.ac.uk/pdbe/static/entry/${id.toLowerCase()}.cif`, id => `PDBe: ${id} (cif)`, false) : src.params.provider.server.params.encoding === 'cif' ? getDownloadParams(src.params.provider.id, id => `https://files.rcsb.org/download/${id.toUpperCase()}.cif`, id => `RCSB: ${id} (cif)`, false) - : getDownloadParams(src.params.provider.id, id => `https://models.rcsb.org/${id.toUpperCase()}.bcif`, id => `RCSB: ${id} (bcif)`, true); + : getDownloadParams(src.params.provider.id, id => `https://models.rcsb.org/${id.toUpperCase()}.bcif`, id => `RCSB: ${id} (bcif)`, true) + ); asTrajectory = !!src.params.options.asTrajectory; break; case 'pdb-dev': - downloadParams = getDownloadParams(src.params.provider.id, + downloadParams = await getDownloadParams(src.params.provider.id, id => { const nId = id.toUpperCase().startsWith('PDBDEV_') ? id : `PDBDEV_${id.padStart(8, '0')}`; return src.params.provider.encoding === 'bcif' @@ -117,19 +130,34 @@ const DownloadStructure = StateAction.build({ asTrajectory = !!src.params.options.asTrajectory; break; case 'swissmodel': - downloadParams = getDownloadParams(src.params.id, id => `https://swissmodel.expasy.org/repository/uniprot/${id.toUpperCase()}.pdb`, id => `SWISS-MODEL: ${id}`, false); + downloadParams = await getDownloadParams(src.params.id, id => `https://swissmodel.expasy.org/repository/uniprot/${id.toUpperCase()}.pdb`, id => `SWISS-MODEL: ${id}`, false); asTrajectory = !!src.params.options.asTrajectory; format = 'pdb'; break; + case 'alphafolddb': + downloadParams = await getDownloadParams(src.params.id, async id => { + const url = `https://www.alphafold.ebi.ac.uk/api/prediction/${id.toUpperCase()}`; + const info = await plugin.runTask(plugin.fetch({ url, type: 'json' })); + if (Array.isArray(info) && info.length > 0) return info[0].cifUrl; + throw new Error(`No AlphaFold DB entry for '${id}'`); + }, id => `AlphaFold DB: ${id}`, false); + asTrajectory = !!src.params.options.asTrajectory; + format = 'mmcif'; + break; + case 'modelarchive': + downloadParams = await getDownloadParams(src.params.id, id => `https://www.modelarchive.org/doi/10.5452/${id.toLowerCase()}.cif`, id => `Model Archive: ${id}`, false); + asTrajectory = !!src.params.options.asTrajectory; + format = 'mmcif'; + break; case 'pubchem': - downloadParams = getDownloadParams(src.params.id, id => `https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/CID/${id.trim()}/record/SDF/?record_type=3d`, id => `PubChem: ${id}`, false); + downloadParams = await getDownloadParams(src.params.id, id => `https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/CID/${id.trim()}/record/SDF/?record_type=3d`, id => `PubChem: ${id}`, false); asTrajectory = !!src.params.options.asTrajectory; format = 'mol'; break; default: throw new Error(`${(src as any).name} not supported.`); } - const representationPreset: any = params.source.params.options.representation || PresetStructureRepresentations.auto.id; + const representationPreset: any = params.source.params.options.representation || plugin.config.get(PluginConfig.Structure.DefaultRepresentationPreset) || PresetStructureRepresentations.auto.id; const showUnitcell = representationPreset !== PresetStructureRepresentations.empty.id; const structure = src.params.options.type.name === 'auto' ? void 0 : src.params.options.type; @@ -151,7 +179,11 @@ const DownloadStructure = StateAction.build({ } else { for (const download of downloadParams) { const data = await plugin.builders.data.download(download, { state: { isGhost: true } }); - const trajectory = await plugin.builders.structure.parseTrajectory(data, format); + const provider = format === 'auto' + ? plugin.dataFormats.auto(getFileInfo(Asset.getUrl(download.url)), data.cell?.obj!) + : plugin.dataFormats.get(format); + if (!provider) throw new Error('unknown file format'); + const trajectory = await plugin.builders.structure.parseTrajectory(data, provider); await plugin.builders.structure.hierarchy.applyPreset(trajectory, 'default', { structure, @@ -164,11 +196,11 @@ const DownloadStructure = StateAction.build({ }).runInContext(ctx); })); -function getDownloadParams(src: string, url: (id: string) => string, label: (id: string) => string, isBinary: boolean): StateTransformer.Params<Download>[] { +async function getDownloadParams(src: string, url: (id: string) => string | Promise<string>, label: (id: string) => string, isBinary: boolean): Promise<StateTransformer.Params<Download>[]> { const ids = src.split(/[,\s]/).map(id => id.trim()).filter(id => !!id && (id.length >= 4 || /^[1-9][0-9]*$/.test(id))); const ret: StateTransformer.Params<Download>[] = []; for (const id of ids) { - ret.push({ url: Asset.Url(url(id)), isBinary, label: label(id) }); + ret.push({ url: Asset.Url(await url(id)), isBinary, label: label(id) }); } return ret; } @@ -176,7 +208,7 @@ function getDownloadParams(src: string, url: (id: string) => string, label: (id: export const UpdateTrajectory = StateAction.build({ display: { name: 'Update Trajectory' }, params: { - action: PD.Select<'advance' | 'reset'>('advance', [['advance', 'Advance'], ['reset', 'Reset']]), + action: PD.Select('advance', PD.arrayToOptions(['advance', 'reset'] as const)), by: PD.Optional(PD.Numeric(1, { min: -1, max: 1, step: 1 })) } })(({ params, state }) => { diff --git a/src/mol-plugin-state/builder/structure/hierarchy-preset.ts b/src/mol-plugin-state/builder/structure/hierarchy-preset.ts index c5c68c60aeb668f8d93fb8ebacd1a54710d7a756..b4b531e1585bb4057ae7a544c4949893f8beee8c 100644 --- a/src/mol-plugin-state/builder/structure/hierarchy-preset.ts +++ b/src/mol-plugin-state/builder/structure/hierarchy-preset.ts @@ -17,6 +17,7 @@ import { Vec3 } from '../../../mol-math/linear-algebra'; import { Model } from '../../../mol-model/structure'; import { getStructureQuality } from '../../../mol-repr/util'; import { OperatorNameColorThemeProvider } from '../../../mol-theme/color/operator-name'; +import { PluginConfig } from '../../../mol-plugin/config'; export interface TrajectoryHierarchyPresetProvider<P = any, S = {}> extends PresetProvider<PluginStateObject.Molecule.Trajectory, P, S> { } export function TrajectoryHierarchyPresetProvider<P, S>(preset: TrajectoryHierarchyPresetProvider<P, S>) { return preset; } @@ -61,7 +62,8 @@ const defaultPreset = TrajectoryHierarchyPresetProvider({ const structureProperties = await builder.insertStructureProperties(structure, params.structureProperties); const unitcell = params.showUnitcell === void 0 || !!params.showUnitcell ? await builder.tryCreateUnitcell(modelProperties, undefined, { isHidden: true }) : void 0; - const representation = await plugin.builders.structure.representation.applyPreset(structureProperties, params.representationPreset || 'auto', params.representationPresetParams); + const representationPreset = params.representationPreset || plugin.config.get(PluginConfig.Structure.DefaultRepresentationPreset) || PresetStructureRepresentations.auto.id; + const representation = await plugin.builders.structure.representation.applyPreset(structureProperties, representationPreset, params.representationPresetParams); return { model, @@ -112,7 +114,8 @@ const allModels = TrajectoryHierarchyPresetProvider({ structures.push(structure); const quality = structure.obj ? getStructureQuality(structure.obj.data, { elementCountFactor: tr.frameCount }) : 'medium'; - await builder.representation.applyPreset(structureProperties, params.representationPreset || 'auto', { theme: { globalName: 'model-index' }, quality }); + const representationPreset = params.representationPreset || plugin.config.get(PluginConfig.Structure.DefaultRepresentationPreset) || PresetStructureRepresentations.auto.id; + await builder.representation.applyPreset(structureProperties, representationPreset, { theme: { globalName: 'model-index' }, quality }); } return { models, structures }; @@ -137,7 +140,8 @@ async function applyCrystalSymmetry(props: { ijkMin: Vec3, ijkMax: Vec3, theme?: const structureProperties = await builder.insertStructureProperties(structure, params.structureProperties); const unitcell = await builder.tryCreateUnitcell(modelProperties, undefined, { isHidden: false }); - const representation = await plugin.builders.structure.representation.applyPreset(structureProperties, params.representationPreset || 'auto', { theme: { globalName: props.theme } }); + const representationPreset = params.representationPreset || plugin.config.get(PluginConfig.Structure.DefaultRepresentationPreset) || PresetStructureRepresentations.auto.id; + const representation = await plugin.builders.structure.representation.applyPreset(structureProperties, representationPreset, { theme: { globalName: props.theme } }); return { model, @@ -207,7 +211,8 @@ const crystalContacts = TrajectoryHierarchyPresetProvider({ const structureProperties = await builder.insertStructureProperties(structure, params.structureProperties); const unitcell = await builder.tryCreateUnitcell(modelProperties, undefined, { isHidden: true }); - const representation = await plugin.builders.structure.representation.applyPreset(structureProperties, params.representationPreset || 'auto', { theme: { globalName: 'operator-name', carbonColor: 'operator-name', focus: { name: 'element-symbol', params: { carbonColor: { name: 'operator-name', params: OperatorNameColorThemeProvider.defaultValues } } } } }); + const representationPreset = params.representationPreset || plugin.config.get(PluginConfig.Structure.DefaultRepresentationPreset) || PresetStructureRepresentations.auto.id; + const representation = await plugin.builders.structure.representation.applyPreset(structureProperties, representationPreset, { theme: { globalName: 'operator-name', carbonColor: 'operator-name', focus: { name: 'element-symbol', params: { carbonColor: { name: 'operator-name', params: OperatorNameColorThemeProvider.defaultValues } } } } }); return { model, diff --git a/src/mol-plugin-state/manager/structure/component.ts b/src/mol-plugin-state/manager/structure/component.ts index 2494fa2970282e14bbf5a2481dd88ee1ed3cb371..5528026b8f60cee36102857a1f20f6673ca76b15 100644 --- a/src/mol-plugin-state/manager/structure/component.ts +++ b/src/mol-plugin-state/manager/structure/component.ts @@ -14,7 +14,7 @@ import { StateBuilder, StateObjectRef, StateTransformer } from '../../../mol-sta import { Task } from '../../../mol-task'; import { ColorTheme } from '../../../mol-theme/color'; import { SizeTheme } from '../../../mol-theme/size'; -import { UUID } from '../../../mol-util'; +import { shallowEqual, UUID } from '../../../mol-util'; import { ColorNames } from '../../../mol-util/color/names'; import { objectForEach } from '../../../mol-util/object'; import { ParamDefinition as PD } from '../../../mol-util/param-definition'; @@ -82,7 +82,7 @@ class StructureComponentManager extends StatefulPluginComponent<StructureCompone if (r.cell.transform.transformer !== StructureRepresentation3D) continue; const params = r.cell.transform.params as StateTransformer.Params<StructureRepresentation3D>; - if (!!params.type.params.ignoreHydrogens !== ignoreHydrogens || params.type.params.quality !== quality || params.type.params.material !== material) { + if (!!params.type.params.ignoreHydrogens !== ignoreHydrogens || params.type.params.quality !== quality || !shallowEqual(params.type.params.material, material)) { update.to(r.cell).update(old => { old.type.params.ignoreHydrogens = ignoreHydrogens; old.type.params.quality = quality; diff --git a/src/mol-plugin-ui/index.ts b/src/mol-plugin-ui/index.ts index 665d76d8659a6c3f2944e518a3505072f0b914ee..d7c62cc94b0a6c3e74671343ed4bc40eaa0fd518 100644 --- a/src/mol-plugin-ui/index.ts +++ b/src/mol-plugin-ui/index.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal <david.sehnal@gmail.com> * @author Alexander Rose <alexander.rose@weirdbyte.de> @@ -13,16 +13,16 @@ import { DefaultPluginUISpec, PluginUISpec } from './spec'; export function createPlugin(target: HTMLElement, spec?: PluginUISpec): PluginUIContext { const ctx = new PluginUIContext(spec || DefaultPluginUISpec()); - ctx.init(); - ReactDOM.render(React.createElement(Plugin, { plugin: ctx }), target); + ctx.init().then(() => { + ReactDOM.render(React.createElement(Plugin, { plugin: ctx }), target); + }); return ctx; } /** Returns the instance of the plugin after all behaviors have been initialized */ export async function createPluginAsync(target: HTMLElement, spec?: PluginUISpec) { const ctx = new PluginUIContext(spec || DefaultPluginUISpec()); - const init = ctx.init(); + await ctx.init(); ReactDOM.render(React.createElement(Plugin, { plugin: ctx }), target); - await init; return ctx; } \ No newline at end of file diff --git a/src/mol-plugin/config.ts b/src/mol-plugin/config.ts index e97446b064f0fe10c49cec80afa9464b420bf657..4e12347af3d4241dfe62190f1176446d24676e8e 100644 --- a/src/mol-plugin/config.ts +++ b/src/mol-plugin/config.ts @@ -84,6 +84,7 @@ export const PluginConfig = { }, Structure: { SizeThresholds: item('structure.size-thresholds', Structure.DefaultSizeThresholds), + DefaultRepresentationPreset: item<string>('structure.default-representation-preset', 'auto'), DefaultRepresentationPresetParams: item<StructureRepresentationPresetProvider.CommonParams>('structure.default-representation-preset-params', { }) } };