diff --git a/src/mol-app/ui/visualization/sequence-view.tsx b/src/mol-app/ui/visualization/sequence-view.tsx index 640f81a6d5e9d6d6bb2904d0338e06434faa1345..2084dcd6c1305ed52e005af96dc844c33e37b819 100644 --- a/src/mol-app/ui/visualization/sequence-view.tsx +++ b/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>; diff --git a/src/mol-model-props/pdbe/structure-quality-report.ts b/src/mol-model-props/pdbe/structure-quality-report.ts index 2f9858c7b301014be7ba25048a60a7a2262b9c2b..4d88557fd23cb292e79f195970a520296cb950e8 100644 --- a/src/mol-model-props/pdbe/structure-quality-report.ts +++ b/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; + } } \ No newline at end of file diff --git a/src/mol-model/structure/export/mmcif.ts b/src/mol-model/structure/export/mmcif.ts index e0a071af7fd5ee59cf9093910c5513cdf909f1e6..a527b835bbe46fba9a167eeabda3531fa145ea42 100644 --- a/src/mol-model/structure/export/mmcif.ts +++ b/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]; diff --git a/src/mol-model/structure/model/properties/custom.ts b/src/mol-model/structure/model/properties/custom.ts index d9a06eee1c7a5f4e19ae8cc462117427a020ca4a..6c8764094f0adb0898fd39152e72a32105c31501 100644 --- a/src/mol-model/structure/model/properties/custom.ts +++ b/src/mol-model/structure/model/properties/custom.ts @@ -5,4 +5,5 @@ */ export * from './custom/descriptor' -export * from './custom/collection' \ No newline at end of file +export * from './custom/collection' +export * from './custom/residue' \ No newline at end of file diff --git a/src/mol-model/structure/model/properties/custom/residue.ts b/src/mol-model/structure/model/properties/custom/residue.ts new file mode 100644 index 0000000000000000000000000000000000000000..7c84a6b4065b00d9ea6e031b8660180246b1d37c --- /dev/null +++ b/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; + } +} \ No newline at end of file diff --git a/src/mol-model/structure/structure/structure.ts b/src/mol-model/structure/structure/structure.ts index 05c5257a3866c809e73e3e7743bfd13045d16068..233d73f9821d364a742c10199e5c3240ee22c036 100644 --- a/src/mol-model/structure/structure/structure.ts +++ b/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; } diff --git a/src/mol-model/structure/structure/symmetry.ts b/src/mol-model/structure/structure/symmetry.ts index 227d5c16d44252d0895d316ead9194eacf966618..e3e9c4b6b4b3a8fa45a5a8dc8aef1f7ded41598b 100644 --- a/src/mol-model/structure/structure/symmetry.ts +++ b/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; diff --git a/src/mol-script/runtime/macro.ts b/src/mol-script/runtime/macro.ts index d6ef5f71d25d6a9feec022ac5aa92bca7b445902..900aaec92da05a463c3c4b6da346e12d7978908d 100644 --- a/src/mol-script/runtime/macro.ts +++ b/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; // } diff --git a/src/mol-script/script/mol-script/symbols.ts b/src/mol-script/script/mol-script/symbols.ts index a647c8d0ce12bef1b162bbaba283c1ccf5b38eee..0e2f2a741d8c859c1a74e8ab9c9016475192e1e9 100644 --- a/src/mol-script/script/mol-script/symbols.ts +++ b/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; } diff --git a/src/mol-util/uuid.ts b/src/mol-util/uuid.ts index b3dc51a34c377aa27e53e7971456f061b163bb2d..4c77f7de6120c640cb8a025adf32ffae7429f6a5 100644 --- a/src/mol-util/uuid.ts +++ b/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 { diff --git a/src/perf-tests/mol-script.ts b/src/perf-tests/mol-script.ts index f05a1d8f26b6bf547b502804da0e9321700e7c6d..219667197ecec3098875c45e8fbb91629d814d94 100644 --- a/src/perf-tests/mol-script.ts +++ b/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(),