Browse Source

wip custom properties (residue prop helpers, query props)

David Sehnal 6 years ago
parent
commit
d293eadc0a

+ 1 - 1
src/mol-app/ui/visualization/sequence-view.tsx

@@ -17,7 +17,7 @@ export class SequenceView extends View<SequenceViewController, {}, {}> {
         const s = this.controller.latestState.structure;
         if (!s) return <div className='molstar-sequence-view-wrap'>No structure available.</div>;
 
-        const seqs = Structure.getModels(s)[0].sequence.sequences;
+        const seqs = s.models[0].sequence.sequences;
         return <div className='molstar-sequence-view-wrap'>
             {seqs.map((seq, i) => <EntitySequence key={i} ctx={this.controller.context} seq={seq} structure={s} /> )}
         </div>;

+ 28 - 42
src/mol-model-props/pdbe/structure-quality-report.ts

@@ -4,16 +4,21 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { Segmentation } from 'mol-data/int';
 import { CifWriter } from 'mol-io/writer/cif';
-import { Model, ModelPropertyDescriptor, ResidueIndex, Structure, StructureElement, Unit } from 'mol-model/structure';
+import { Model, ModelPropertyDescriptor, ResidueIndex, Unit, ResidueCustomProperty } from 'mol-model/structure';
 import { residueIdFields } from 'mol-model/structure/export/categories/atom_site';
 import CifField = CifWriter.Field;
 import { mmCIF_residueId_schema } from 'mol-io/reader/cif/schema/mmcif-extras';
 import { Column, Table } from 'mol-data/db';
 import { toTable } from 'mol-io/reader/cif/schema';
+import { StructureElement } from 'mol-model/structure/structure';
 
-type IssueMap = Map<ResidueIndex, string[]>
+
+import { QuerySymbolRuntime } from 'mol-script/runtime/query/compiler';
+import { CustomPropSymbol } from 'mol-script/language/symbol';
+import Type from 'mol-script/language/type';
+
+type IssueMap = ResidueCustomProperty<string[]>
 
 const _Descriptor: ModelPropertyDescriptor = {
     isStatic: true,
@@ -30,55 +35,27 @@ const _Descriptor: ModelPropertyDescriptor = {
             instance(ctx) {
                 const issues = StructureQualityReport.get(ctx.model);
                 if (!issues) return CifWriter.Category.Empty;
-
-                const residues = getResidueLoci(ctx.structure, issues);
-                return {
-                    fields: _structure_quality_report_issues_fields,
-                    data: <ExportCtx>{ model: ctx.model, residues, residueIndex: ctx.model.atomicHierarchy.residueAtomSegments.index, issues },
-                    rowCount: residues.length
-                };
+                return ResidueCustomProperty.createCifCategory(ctx, issues, _structure_quality_report_issues_fields);
             }
         }]
+    },
+    symbols: {
+        issueCount: QuerySymbolRuntime.Dynamic(CustomPropSymbol('pdbe', 'structure-quality.issue-count', Type.Num),
+            ctx => StructureQualityReport.getIssues(ctx.element).length)
     }
 }
 
-type ExportCtx = { model: Model, residueIndex: ArrayLike<ResidueIndex>, residues: StructureElement[], issues: IssueMap };
-
-const _structure_quality_report_issues_fields: CifField<ResidueIndex, ExportCtx>[] = [
+type ExportCtx = ResidueCustomProperty.ExportCtx<string[]>
+const _structure_quality_report_issues_fields: CifField<number, ExportCtx>[] = [
     CifField.index('id'),
-    ...residueIdFields<ResidueIndex, ExportCtx>((k, d) => d.residues[k]),
-    CifField.str<ResidueIndex, ExportCtx>('issues', (i, d) => d.issues.get(d.residueIndex[d.residues[i].element])!.join(','))
+    ...residueIdFields<number, ExportCtx>((i, d) => d.elements[i]),
+    CifField.str<number, ExportCtx>('issues', (i, d) => d.property(i).join(','))
 ];
 
 const _structure_quality_report_fields: CifField<ResidueIndex, ExportCtx>[] = [
     CifField.str('updated_datetime_utc', () => `${new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '')}`)
 ];
 
-function getResidueLoci(structure: Structure, issues: IssueMap) {
-    const seenResidues = new Set<ResidueIndex>();
-    const unitGroups = structure.unitSymmetryGroups;
-    const loci: StructureElement[] = [];
-
-    for (const unitGroup of unitGroups) {
-        const unit = unitGroup.units[0];
-        if (!Unit.isAtomic(unit)) {
-            continue;
-        }
-
-        const residues = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, unit.elements);
-        while (residues.hasNext) {
-            const seg = residues.move();
-            if (!issues.has(seg.index) || seenResidues.has(seg.index)) continue;
-
-            seenResidues.add(seg.index);
-            loci[loci.length] = StructureElement.create(unit, unit.elements[seg.start]);
-        }
-    }
-
-    loci.sort((x, y) => x.element - y.element);
-    return loci;
-}
-
 function createIssueMapFromJson(modelData: Model, data: any): IssueMap | undefined {
     const ret = new Map<ResidueIndex, string[]>();
     if (!data.molecules) return;
@@ -100,7 +77,7 @@ function createIssueMapFromJson(modelData: Model, data: any): IssueMap | undefin
         }
     }
 
-    return ret;
+    return ResidueCustomProperty.fromMap(ret, Unit.Kind.Atomic);
 }
 
 function createIssueMapFromCif(modelData: Model, data: Table<typeof StructureQualityReport.Schema.pdbe_structure_quality_report_issues>): IssueMap | undefined {
@@ -112,7 +89,7 @@ function createIssueMapFromCif(modelData: Model, data: Table<typeof StructureQua
         ret.set(idx, issues.value(i));
     }
 
-    return ret;
+    return ResidueCustomProperty.fromMap(ret, Unit.Kind.Atomic);
 }
 
 export namespace StructureQualityReport {
@@ -158,4 +135,13 @@ export namespace StructureQualityReport {
     export function get(model: Model): IssueMap | undefined {
         return model._staticPropertyData.__StructureQualityReport__;
     }
+
+    const _emptyArray: string[] = [];
+    export function getIssues(e: StructureElement) {
+        if (!Unit.isAtomic(e.unit)) return _emptyArray;
+        const issues = StructureQualityReport.get(e.unit.model);
+        if (!issues) return _emptyArray;
+        const rI = e.unit.residueIndex[e.element];
+        return issues.has(rI) ? issues.get(rI)! : _emptyArray;
+    }
 }

+ 1 - 1
src/mol-model/structure/export/mmcif.ts

@@ -88,7 +88,7 @@ export const mmCIF_Export_Filters = {
 
 /** Doesn't start a data block */
 export function encode_mmCIF_categories(encoder: CifWriter.Encoder, structure: Structure) {
-    const models = Structure.getModels(structure);
+    const models = structure.models;
     if (models.length !== 1) throw 'Can\'t export stucture composed from multiple models.';
     const model = models[0];
 

+ 2 - 1
src/mol-model/structure/model/properties/custom.ts

@@ -5,4 +5,5 @@
  */
 
 export * from './custom/descriptor'
-export * from './custom/collection'
+export * from './custom/collection'
+export * from './custom/residue'

+ 94 - 0
src/mol-model/structure/model/properties/custom/residue.ts

@@ -0,0 +1,94 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { ResidueIndex } from '../../indexing';
+import { Unit, Structure, StructureElement } from '../../../structure';
+import { Segmentation } from 'mol-data/int';
+import { CifExportContext } from '../../../export/mmcif';
+import { UUID } from 'mol-util';
+import { CifWriter } from 'mol-io/writer/cif';
+
+export interface ResidueCustomProperty<T = any> {
+    readonly id: UUID,
+    readonly kind: Unit.Kind,
+    has(idx: ResidueIndex): boolean
+    get(idx: ResidueIndex): T | undefined
+}
+
+export namespace ResidueCustomProperty {
+    export interface ExportCtx<T> {
+        exportCtx: CifExportContext,
+        elements: StructureElement[],
+        property(index: number): T
+    };
+
+    function getExportCtx<T>(exportCtx: CifExportContext, prop: ResidueCustomProperty<T>): ExportCtx<T> {
+        if (exportCtx.cache[prop.id]) return exportCtx.cache[prop.id];
+        const residueIndex = exportCtx.model.atomicHierarchy.residueAtomSegments.index;
+        const elements = getStructureElements(exportCtx.structure, prop);
+        return {
+            exportCtx,
+            elements,
+            property: i => prop.get(residueIndex[elements[i].element])!
+        }
+    }
+
+    export function createCifCategory<T>(ctx: CifExportContext, prop: ResidueCustomProperty<T>, fields: CifWriter.Field<number, ExportCtx<T>>[]): CifWriter.Category.Instance {
+        const data = getExportCtx(ctx, prop);
+        return { fields, data, rowCount: data.elements.length };
+    }
+
+    class FromMap<T> implements ResidueCustomProperty<T> {
+        readonly id = UUID.create();
+
+        has(idx: ResidueIndex): boolean {
+            return this.map.has(idx);
+        }
+
+        get(idx: ResidueIndex) {
+            return this.map.get(idx);
+        }
+
+        constructor(private map: Map<ResidueIndex, T>, public kind: Unit.Kind) {
+        }
+    }
+
+    export function fromMap<T>(map: Map<ResidueIndex, T>, kind: Unit.Kind) {
+        return new FromMap(map, kind);
+    }
+
+    /**
+     * Gets all StructureElements that correspond to 1st atoms of residues that have an property assigned.
+     * Only works correctly for structures with a single model.
+     */
+    export function getStructureElements(structure: Structure, property: ResidueCustomProperty) {
+        const models = structure.models;
+        if (models.length !== 1) throw new Error(`Only works on structures with a single model.`);
+
+        const seenResidues = new Set<ResidueIndex>();
+        const unitGroups = structure.unitSymmetryGroups;
+        const loci: StructureElement[] = [];
+
+        for (const unitGroup of unitGroups) {
+            const unit = unitGroup.units[0];
+            if (unit.kind !== property.kind) {
+                continue;
+            }
+
+            const residues = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, unit.elements);
+            while (residues.hasNext) {
+                const seg = residues.move();
+                if (!property.has(seg.index) || seenResidues.has(seg.index)) continue;
+
+                seenResidues.add(seg.index);
+                loci[loci.length] = StructureElement.create(unit, unit.elements[seg.start]);
+            }
+        }
+
+        loci.sort((x, y) => x.element - y.element);
+        return loci;
+    }
+}

+ 17 - 10
src/mol-model/structure/structure/structure.ts

@@ -32,8 +32,9 @@ class Structure {
         crossLinkRestraints?: CrossLinkRestraints,
         unitSymmetryGroups?: ReadonlyArray<Unit.SymmetryGroup>,
         carbohydrates?: Carbohydrates,
+        models?: ReadonlyArray<Model>,
         hashCode: number,
-        elementCount: number
+        elementCount: number,
     } = { hashCode: -1, elementCount: 0 };
 
     subsetBuilder(isSorted: boolean) {
@@ -102,6 +103,12 @@ class Structure {
         return this._props.carbohydrates;
     }
 
+    get models(): ReadonlyArray<Model> {
+        if (this._props.models) return this._props.models;
+        this._props.models = getModels(this);
+        return this._props.models;
+    }
+
     constructor(units: ArrayLike<Unit>) {
         const map = IntMap.Mutable<Unit>();
         let elementCount = 0;
@@ -123,6 +130,15 @@ class Structure {
 
 function cmpUnits(units: ArrayLike<Unit>, i: number, j: number) { return units[i].id - units[j].id; }
 
+function getModels(s: Structure) {
+    const { units } = s;
+    const arr = UniqueArray.create<Model['id'], Model>();
+    for (const u of units) {
+        UniqueArray.add(arr, u.model.id, u.model);
+    }
+    return arr.array;
+}
+
 namespace Structure {
     export const Empty = new Structure([]);
 
@@ -199,15 +215,6 @@ namespace Structure {
 
     export function Builder() { return new StructureBuilder(); }
 
-    export function getModels(s: Structure) {
-        const { units } = s;
-        const arr = UniqueArray.create<Model['id'], Model>();
-        for (const u of units) {
-            UniqueArray.add(arr, u.model.id, u.model);
-        }
-        return arr.array;
-    }
-
     export function hashCode(s: Structure) {
         return s.hashCode;
     }

+ 4 - 4
src/mol-model/structure/structure/symmetry.ts

@@ -17,7 +17,7 @@ import { SymmetryOperator, Spacegroup, SpacegroupCell } from 'mol-math/geometry'
 namespace StructureSymmetry {
     export function buildAssembly(structure: Structure, asmName: string) {
         return Task.create('Build Assembly', async ctx => {
-            const models = Structure.getModels(structure);
+            const models = structure.models;
             if (models.length !== 1) throw new Error('Can only build assemblies from structures based on 1 model.');
 
             const assembly = ModelSymmetry.findAssembly(models[0], asmName);
@@ -121,7 +121,7 @@ function assembleOperators(structure: Structure, operators: ReadonlyArray<Symmet
 }
 
 async function _buildNCS(ctx: RuntimeContext, structure: Structure) {
-    const models = Structure.getModels(structure);
+    const models = structure.models;
     if (models.length !== 1) throw new Error('Can only build NCS from structures based on 1 model.');
 
     const operators = models[0].symmetry.ncsOperators;
@@ -130,7 +130,7 @@ async function _buildNCS(ctx: RuntimeContext, structure: Structure) {
 }
 
 async function findSymmetryRange(ctx: RuntimeContext, structure: Structure, ijkMin: Vec3, ijkMax: Vec3) {
-    const models = Structure.getModels(structure);
+    const models = structure.models;
     if (models.length !== 1) throw new Error('Can only build symmetries from structures based on 1 model.');
 
     const { spacegroup } = models[0].symmetry;
@@ -141,7 +141,7 @@ async function findSymmetryRange(ctx: RuntimeContext, structure: Structure, ijkM
 }
 
 async function findMatesRadius(ctx: RuntimeContext, structure: Structure, radius: number) {
-    const models = Structure.getModels(structure);
+    const models = structure.models;
     if (models.length !== 1) throw new Error('Can only build symmetries from structures based on 1 model.');
 
     const symmetry = models[0].symmetry;

+ 1 - 1
src/mol-script/runtime/macro.ts

@@ -30,7 +30,7 @@
 //         }
 
 //         const head = subst(table, expr.head, argIndex, args);
-//         const headChanged = head === expr.head;
+//         const headChanged = head !== expr.head;
 //         if (!expr.args) {
 //             return headChanged ? Expression.Apply(head) : expr;
 //         }

+ 1 - 1
src/mol-script/script/mol-script/symbols.ts

@@ -301,7 +301,7 @@ function substSymbols(expr: Expression): Expression {
     }
 
     const head = substSymbols(expr.head);
-    const headChanged = head === expr.head;
+    const headChanged = head !== expr.head;
     if (!expr.args) {
         return headChanged ? Expression.Apply(head) : expr;
     }

+ 1 - 1
src/mol-util/uuid.ts

@@ -6,7 +6,7 @@
 
 import { now } from 'mol-task'
 
-interface UUID extends String { '@type': 'uuid' }
+type UUID = string & { '@type': 'uuid' }
 
 namespace UUID {
     export function create(): UUID {

+ 13 - 1
src/perf-tests/mol-script.ts

@@ -8,6 +8,8 @@ import { parseMolScript } from 'mol-script/language/parser';
 import * as util from 'util'
 import { transpileMolScript } from 'mol-script/script/mol-script/symbols';
 import { formatMolScript } from 'mol-script/language/expression-formatter';
+import { StructureQualityReport } from 'mol-model-props/pdbe/structure-quality-report';
+import fetch from 'node-fetch'; 
 
 // import Examples from 'mol-script/script/mol-script/examples'
 // import { parseMolScript } from 'mol-script/script/mol-script/parser'
@@ -24,8 +26,9 @@ import { formatMolScript } from 'mol-script/language/expression-formatter';
 //     ;; this is a comment
 //     ((hi) (ho))`);
 
+//;; :residue-test (= atom.label_comp_id REA)
 const exprs = parseMolScript(`(sel.atom.atom-groups
-    :residue-test (= atom.label_comp_id REA)
+    :residue-test (> pdbe.structure-quality.issue-count 0)
     :atom-test (= atom.el _C))`);
 
 const tsp = transpileMolScript(exprs[0]);
@@ -58,10 +61,19 @@ const CustomProp = ModelPropertyDescriptor({
 
 DefaultQueryRuntimeTable.addCustomProp(CustomProp);
 
+DefaultQueryRuntimeTable.addCustomProp(StructureQualityReport.Descriptor);
+
 export async function testQ() {
     const frame = await readCifFile('e:/test/quick/1cbs_updated.cif');
     const { structure } = await getModelsAndStructure(frame);
 
+    await StructureQualityReport.attachFromCifOrApi(structure.models[0], {
+        PDBe_apiSourceJson: async model => {
+            const rawData = await fetch(`https://www.ebi.ac.uk/pdbe/api/validation/residuewise_outlier_summary/entry/${model.label.toLowerCase()}`, { timeout: 1500 });
+            return await rawData.json();
+        }
+    })
+
     let expr = MolScriptBuilder.struct.generator.atomGroups({
         'atom-test': MolScriptBuilder.core.rel.eq([
             MolScriptBuilder.struct.atomProperty.core.elementSymbol(),