Browse Source

Merge pull request #309 from molstar/ma-support

ModelArchive / AlphaFold support
Alexander Rose 3 years ago
parent
commit
812eb0efa9

+ 14 - 0
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
 

+ 63 - 1
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
+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

+ 6 - 0
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>

+ 69 - 5
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
-}
+}
+
+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);
+        }
+    }
+});

+ 10 - 17
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';

+ 212 - 0
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);
+    }
+});

+ 106 - 0
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);
+                }
+            }
+        }
+    }
+};

+ 95 - 0
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);
+                }
+            }
+        }
+    }
+};

+ 131 - 0
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);
+    }
+});

+ 326 - 1
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;

+ 54 - 22
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 }) => {

+ 9 - 4
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,

+ 2 - 2
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;

+ 5 - 5
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;
 }

+ 1 - 0
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', { })
     }
 };