Ver Fonte

add ModelArchive extension

- QualityAssessment prop (from ma_qa_metric_local mmcif category)
- pLDDT & qmean coloring
- pLDDT & qmean repr presets
- pLDDT & qmean molql symbol
- pLDDT & qmean loci labels (including avg for mutli residue selections)
- pLDDT selection query
Alexander Rose há 3 anos atrás
pai
commit
84fe2d2502

+ 3 - 1
src/apps/viewer/index.ts

@@ -10,6 +10,7 @@ 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 { Mp4Export } from '../../extensions/mp4-export';
 import { PDBeStructureQualityReport } from '../../extensions/pdbe';
 import { RCSBAssemblySymmetry, RCSBValidationReport } from '../../extensions/rcsb';
@@ -60,7 +61,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 = {

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

+ 100 - 0
src/extensions/model-archive/quality-assessment/color/plddt.ts

@@ -0,0 +1,100 @@
+/**
+ * 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 { 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 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);
+            }
+            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);
+                }
+            }
+        }
+    }
+};

+ 89 - 0
src/extensions/model-archive/quality-assessment/color/qmean.ts

@@ -0,0 +1,89 @@
+/**
+ * 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 { 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 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);
+            }
+            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);
+    }
+});