diff --git a/src/mol-model-props/common/wrapper.ts b/src/mol-model-props/common/wrapper.ts new file mode 100644 index 0000000000000000000000000000000000000000..6265ca85049af7cc2af6316b22e4dbe4a0bc8677 --- /dev/null +++ b/src/mol-model-props/common/wrapper.ts @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { CifWriter } from 'mol-io/writer/cif'; +import { Model } from 'mol-model/structure'; +import { CifExportContext } from 'mol-model/structure'; +import { dateToUtcString } from 'mol-util/date'; + +interface PropertyWrapper<Data> { + info: PropertyWrapper.Info, + data: Data +} + +namespace PropertyWrapper { + export interface Info { + timestamp_utc: string + } + + export function createInfo(): Info { + return { timestamp_utc: dateToUtcString(new Date()) }; + } + + export function defaultInfoCategory(name: string, getter: (model: Model) => PropertyWrapper<unknown> | undefined): CifWriter.Category<CifExportContext> { + return { + name, + instance(ctx) { + const prop = getter(ctx.firstModel); + if (!prop) return CifWriter.Category.Empty; + return { + fields: _info_fields, + source: [{ data: prop.info.timestamp_utc, rowCount: 1 }] + }; + } + } + } + + const _info_fields: CifWriter.Field<number, string>[] = [ + CifWriter.Field.str('updated_datetime_utc', (_, date) => date) + ]; + + export function tryGetInfoFromCif(categoryName: string, model: Model): Info | undefined { + if (model.sourceData.kind !== 'mmCIF' || !model.sourceData.frame.categoryNames.includes(categoryName)) { + return; + } + + const timestampField = model.sourceData.frame.categories[categoryName].getField('updated_datetime_utc'); + if (!timestampField || timestampField.rowCount === 0) return; + + return { timestamp_utc: timestampField.str(0) || dateToUtcString(new Date()) }; + } +} + +export { PropertyWrapper } \ No newline at end of file diff --git a/src/mol-model-props/index.ts b/src/mol-model-props/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/mol-model-props/pdbe/structure-quality-report.ts b/src/mol-model-props/pdbe/structure-quality-report.ts index 47e874a65c56e610f236f202acdccd7b6d4eab04..d69524a6ab5b22957568a9ef7f973d49c728d633 100644 --- a/src/mol-model-props/pdbe/structure-quality-report.ts +++ b/src/mol-model-props/pdbe/structure-quality-report.ts @@ -4,111 +4,51 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import { CifWriter } from 'mol-io/writer/cif'; -import { Model, ModelPropertyDescriptor, ResidueIndex, Unit, ResidueCustomProperty, StructureProperties as P } 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 { mmCIF_residueId_schema } from 'mol-io/reader/cif/schema/mmcif-extras'; +import { CifWriter } from 'mol-io/writer/cif'; +import { Model, ModelPropertyDescriptor, ResidueCustomProperty, ResidueIndex, StructureProperties as P, Unit } from 'mol-model/structure'; +import { residueIdFields } from 'mol-model/structure/export/categories/atom_site'; import { StructureElement } from 'mol-model/structure/structure'; - - -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: false, - name: 'structure_quality_report', - cifExport: { - prefix: 'pdbe', - categories: [{ - name: 'pdbe_structure_quality_report', - instance(ctx) { - const structure = ctx.structures[0]; - const issues = StructureQualityReport.get(structure.model); - return { - fields: _structure_quality_report_fields, - source: [{ data: issues ? issues.updated : 'n/a', rowCount: 1 }] - }; - } - }, { - name: 'pdbe_structure_quality_report_issues', - instance(ctx) { - return { - fields: _structure_quality_report_issues_fields, - source: ctx.structures.map( - s => ResidueCustomProperty.getCifDataSource(s, StructureQualityReport.getIssueMap(s.model), ctx.cache)) - }; - } - }] - }, - symbols: { - issueCount: QuerySymbolRuntime.Dynamic(CustomPropSymbol('pdbe', 'structure-quality.issue-count', Type.Num), - ctx => StructureQualityReport.getIssues(ctx.element).length), - // TODO: add (hasIssue :: IssueType(extends string) -> boolean) symbol - } -}) - -type ExportCtx = ResidueCustomProperty.ExportCtx<string[]> -const _structure_quality_report_issues_fields: CifField<number, ExportCtx>[] = [ - CifField.index('id'), - ...residueIdFields<number, ExportCtx>((i, d) => d.elements[i]), - CifField.int<number, ExportCtx>('pdbx_PDB_model_num', (i, d) => P.unit.model_num(d.elements[i])), - CifField.str<number, ExportCtx>('issues', (i, d) => d.property(i).join(',')) -]; - -const _structure_quality_report_fields: CifField<number, string>[] = [ - CifField.str('updated_datetime_utc', (_, date) => date) -]; - -function createIssueMapFromJson(modelData: Model, data: any): IssueMap | undefined { - const ret = new Map<ResidueIndex, string[]>(); - if (!data.molecules) return; - - for (const entity of data.molecules) { - const entity_id = entity.entity_id.toString(); - for (const chain of entity.chains) { - const asym_id = chain.struct_asym_id.toString(); - for (const model of chain.models) { - const model_id = model.model_id.toString(); - if (+model_id !== modelData.modelNum) continue; - - for (const residue of model.residues) { - const auth_seq_id = residue.author_residue_number, ins_code = residue.author_insertion_code || ''; - const idx = modelData.atomicHierarchy.index.findResidue(entity_id, asym_id, auth_seq_id, ins_code); - ret.set(idx, residue.outlier_types); - } - } - } - } - - return ResidueCustomProperty.fromMap(ret, Unit.Kind.Atomic); -} - -function createIssueMapFromCif(modelData: Model, data: Table<typeof StructureQualityReport.Schema.pdbe_structure_quality_report_issues>): IssueMap | undefined { - const ret = new Map<ResidueIndex, string[]>(); - const { label_entity_id, label_asym_id, auth_seq_id, pdbx_PDB_ins_code, issues, pdbx_PDB_model_num, _rowCount } = data; - - for (let i = 0; i < _rowCount; i++) { - if (pdbx_PDB_model_num.value(i) !== modelData.modelNum) continue; - const idx = modelData.atomicHierarchy.index.findResidue(label_entity_id.value(i), label_asym_id.value(i), auth_seq_id.value(i), pdbx_PDB_ins_code.value(i)); - ret.set(idx, issues.value(i)); - } - - return ResidueCustomProperty.fromMap(ret, Unit.Kind.Atomic); -} +import { QuerySymbolRuntime } from 'mol-script/runtime/query/compiler'; +import { PropertyWrapper } from '../common/wrapper'; +import CifField = CifWriter.Field; export namespace StructureQualityReport { - export interface Data { - updated: string, - map: IssueMap | undefined + export type IssueMap = ResidueCustomProperty<string[]> + export type Property = PropertyWrapper<IssueMap | undefined> + + export function get(model: Model): Property | undefined { + // must be defined before the descriptor so it's not undefined. + return model._dynamicPropertyData.__StructureQualityReport__; } - export const Descriptor = _Descriptor; + export const Descriptor = ModelPropertyDescriptor({ + isStatic: false, + name: 'structure_quality_report', + cifExport: { + prefix: 'pdbe', + categories: [ + PropertyWrapper.defaultInfoCategory('pdbe_structure_quality_report', StructureQualityReport.get), + { + name: 'pdbe_structure_quality_report_issues', + instance(ctx) { + return { + fields: _structure_quality_report_issues_fields, + source: ctx.structures.map(s => ResidueCustomProperty.getCifDataSource(s, StructureQualityReport.getIssueMap(s.model), ctx.cache)) + }; + } + }] + }, + symbols: { + issueCount: QuerySymbolRuntime.Dynamic(CustomPropSymbol('pdbe', 'structure-quality.issue-count', Type.Num), + ctx => StructureQualityReport.getIssues(ctx.element).length), + // TODO: add (hasIssue :: IssueType(extends string) -> boolean) symbol + } + }); export const Schema = { pdbe_structure_quality_report: { @@ -122,49 +62,96 @@ export namespace StructureQualityReport { } } + function getCifData(model: Model) { + if (model.sourceData.kind !== 'mmCIF') throw new Error('Data format must be mmCIF.'); + return toTable(Schema.pdbe_structure_quality_report_issues, model.sourceData.frame.categories.pdbe_structure_quality_report_issues); + } + export async function attachFromCifOrApi(model: Model, params: { // provide JSON from api PDBe_apiSourceJson?: (model: Model) => Promise<any> }) { if (get(model)) return true; - let issueMap, updated = `${new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '')}`; - if (model.sourceData.kind === 'mmCIF' && model.sourceData.frame.categoryNames.includes('pdbe_structure_quality_report')) { - const data = toTable(Schema.pdbe_structure_quality_report_issues, model.sourceData.frame.categories.pdbe_structure_quality_report_issues); - const f = model.sourceData.frame.categories['pdbe_structure_quality_report'].getField('updated_datetime_utc'); - updated = f ? f.str(0) : updated; + let issueMap: IssueMap | undefined; + let info = PropertyWrapper.tryGetInfoFromCif('pdbe_structure_quality_report', model); + if (info) { + const data = getCifData(model); issueMap = createIssueMapFromCif(model, data); } else if (params.PDBe_apiSourceJson) { const data = await params.PDBe_apiSourceJson(model); if (!data) return false; + info = PropertyWrapper.createInfo(); issueMap = createIssueMapFromJson(model, data); } else { return false; } model.customProperties.add(Descriptor); - (model._dynamicPropertyData.__StructureQualityReport__ as Data) = { - updated, - map: issueMap - }; + set(model, { info, data: issueMap }); return true; } - export function get(model: Model): Data | undefined { - return model._dynamicPropertyData.__StructureQualityReport__; + function set(model: Model, prop: Property) { + (model._dynamicPropertyData.__StructureQualityReport__ as Property) = prop; } export function getIssueMap(model: Model): IssueMap | undefined { - const data = get(model); - return data && data.map; + const prop = get(model); + return prop && prop.data; } const _emptyArray: string[] = []; export function getIssues(e: StructureElement) { if (!Unit.isAtomic(e.unit)) return _emptyArray; - const issues = StructureQualityReport.get(e.unit.model); - if (!issues || !issues.map) return _emptyArray; + const prop = StructureQualityReport.get(e.unit.model); + if (!prop || !prop.data) return _emptyArray; const rI = e.unit.residueIndex[e.element]; - return issues.map.has(rI) ? issues.map.get(rI)! : _emptyArray; + return prop.data.has(rI) ? prop.data.get(rI)! : _emptyArray; } +} + +type ExportCtx = ResidueCustomProperty.ExportCtx<string[]> +const _structure_quality_report_issues_fields: CifField<number, ExportCtx>[] = [ + CifField.index('id'), + ...residueIdFields<number, ExportCtx>((i, d) => d.elements[i]), + CifField.int<number, ExportCtx>('pdbx_PDB_model_num', (i, d) => P.unit.model_num(d.elements[i])), + CifField.str<number, ExportCtx>('issues', (i, d) => d.property(i).join(',')) +]; + +function createIssueMapFromJson(modelData: Model, data: any): StructureQualityReport.IssueMap | undefined { + const ret = new Map<ResidueIndex, string[]>(); + if (!data.molecules) return; + + for (const entity of data.molecules) { + const entity_id = entity.entity_id.toString(); + for (const chain of entity.chains) { + const asym_id = chain.struct_asym_id.toString(); + for (const model of chain.models) { + const model_id = model.model_id.toString(); + if (+model_id !== modelData.modelNum) continue; + + for (const residue of model.residues) { + const auth_seq_id = residue.author_residue_number, ins_code = residue.author_insertion_code || ''; + const idx = modelData.atomicHierarchy.index.findResidue(entity_id, asym_id, auth_seq_id, ins_code); + ret.set(idx, residue.outlier_types); + } + } + } + } + + return ResidueCustomProperty.fromMap(ret, Unit.Kind.Atomic); +} + +function createIssueMapFromCif(modelData: Model, data: Table<typeof StructureQualityReport.Schema.pdbe_structure_quality_report_issues>): StructureQualityReport.IssueMap | undefined { + const ret = new Map<ResidueIndex, string[]>(); + const { label_entity_id, label_asym_id, auth_seq_id, pdbx_PDB_ins_code, issues, pdbx_PDB_model_num, _rowCount } = data; + + for (let i = 0; i < _rowCount; i++) { + if (pdbx_PDB_model_num.value(i) !== modelData.modelNum) continue; + const idx = modelData.atomicHierarchy.index.findResidue(label_entity_id.value(i), label_asym_id.value(i), auth_seq_id.value(i), pdbx_PDB_ins_code.value(i)); + ret.set(idx, issues.value(i)); + } + + return ResidueCustomProperty.fromMap(ret, Unit.Kind.Atomic); } \ No newline at end of file diff --git a/src/mol-model/structure/structure.ts b/src/mol-model/structure/structure.ts index a6a25607900a4ac9912f878f6ac5ddc9abf9541b..46dbdecdcc8a9aec23583dc4fab0a5d3fe79bc97 100644 --- a/src/mol-model/structure/structure.ts +++ b/src/mol-model/structure/structure.ts @@ -12,4 +12,5 @@ import { Link } from './structure/unit/links' import StructureProperties from './structure/properties' export { StructureElement, Link, Structure, Unit, StructureSymmetry, StructureProperties } -export * from './structure/unit/rings' \ No newline at end of file +export * from './structure/unit/rings' +export * from './export/mmcif' \ No newline at end of file diff --git a/src/mol-util/date.ts b/src/mol-util/date.ts new file mode 100644 index 0000000000000000000000000000000000000000..90d03997b579862440fc0dc3a30c8663a6ee48cc --- /dev/null +++ b/src/mol-util/date.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +export function dateToUtcString(date: Date) { + return date.toISOString().replace(/T/, ' ').replace(/\..+/, ''); +} \ No newline at end of file diff --git a/src/servers/model/config.ts b/src/servers/model/config.ts index 1bf830d00639fd568c5adebbe8d9bbda7da03cbd..ae25a2e55d07f23948ae0fe754e333e8082f1830 100644 --- a/src/servers/model/config.ts +++ b/src/servers/model/config.ts @@ -51,7 +51,7 @@ const config = { * Paths (relative to the root directory of the model server) to JavaScript files that specify custom properties */ customPropertyProviders: [ - // './properties/pdbe', + './properties/pdbe', // './properties/rcsb' ], diff --git a/src/servers/model/properties/providers/pdbe.ts b/src/servers/model/properties/providers/pdbe.ts index 751bab2ea6fc56fdf99220976bb905aa930f1b86..bec1c19332f271950b78eb2086eca8d27fdff77b 100644 --- a/src/servers/model/properties/providers/pdbe.ts +++ b/src/servers/model/properties/providers/pdbe.ts @@ -10,9 +10,13 @@ import { StructureQualityReport } from 'mol-model-props/pdbe/structure-quality-r import { fetchRetry } from '../../utils/fetch-retry'; import { UUID } from 'mol-util'; +const USE_FILE_SOURCE = false; + export function PDBe_structureQualityReport(model: Model, cache: any) { return StructureQualityReport.attachFromCifOrApi(model, { - PDBe_apiSourceJson: residuewise_outlier_summary.getDataFromFile + PDBe_apiSourceJson: USE_FILE_SOURCE + ? residuewise_outlier_summary.getDataFromFile + : residuewise_outlier_summary.getDataFromApiProvider(cache) }); } diff --git a/src/servers/model/server/jobs.ts b/src/servers/model/server/jobs.ts index 4019cc654b401c4d7c4f951d14e363e271406226..7697d30c52260acf5d6ba729eb2c6b4a63b3d048 100644 --- a/src/servers/model/server/jobs.ts +++ b/src/servers/model/server/jobs.ts @@ -41,12 +41,12 @@ export function createJob<Name extends QueryName>(definition: JobDefinition<Name if (!queryDefinition) throw new Error(`Query '${definition.queryName}' is not supported.`); const normalizedParams = normalizeQueryParams(queryDefinition, definition.queryParams); - + const sourceId = definition.sourceId || '_local_'; return { id: UUID.create(), datetime_utc: `${new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '')}`, - key: `${definition.sourceId}/${definition.entryId}`, - sourceId: definition.sourceId || '_local_', + key: `${sourceId}/${definition.entryId}`, + sourceId, entryId: definition.entryId, queryDefinition, normalizedParams, diff --git a/src/servers/model/test.ts b/src/servers/model/test.ts index c346381c4da729d786751bcf30f6a4b95a49633b..3e1cc6d9e7e5b0ac0be920df3fbb0ed4bf7d8108 100644 --- a/src/servers/model/test.ts +++ b/src/servers/model/test.ts @@ -42,19 +42,19 @@ if (!fs.existsSync(outPath)) fs.mkdirSync(outPath); async function run() { try { // const testFile = '1crn.cif' - const testFile = '1grm_updated.cif' - // const request = createJob({ - // entryId: path.join(examplesPath, testFile), - // queryName: 'full', - // queryParams: { } - // }); + const testFile = '1cbs_updated.cif' const request = createJob({ entryId: path.join(examplesPath, testFile), - queryName: 'atoms', - queryParams: { - atom_site: { label_comp_id: 'ALA' } - } + queryName: 'full', + queryParams: { } }); + // const request = createJob({ + // entryId: path.join(examplesPath, testFile), + // queryName: 'atoms', + // queryParams: { + // atom_site: { label_comp_id: 'ALA' } + // } + // }); const encoder = await resolveJob(request); const writer = wrapFile(path.join(outPath, testFile)); encoder.writeTo(writer); diff --git a/tsconfig.json b/tsconfig.json index d68886f2655f80a6b402a75e583cec8fdbe969b1..31421faf7852e8d4f240815c857b7b047af3467a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,7 +21,7 @@ "mol-io": ["./mol-io"], "mol-math": ["./mol-math"], "mol-model": ["./mol-model"], - "mol-model-props": ["./mol-model-props"], + "mol-model-props": ["./mol-model-props", "./mol-model-props/index.ts"], "mol-ql": ["./mol-ql"], "mol-script": ["./mol-script"], "mol-task": ["./mol-task", "./mol-task/index.ts"],