diff --git a/src/mol-model-props/pdbe/structure-quality-report.ts b/src/mol-model-props/pdbe/structure-quality-report.ts index c50a298f260afd371cc0d1fbc33795298a9d6315..0cbf120d963ecddf32d1a5a06a79b7619b708192 100644 --- a/src/mol-model-props/pdbe/structure-quality-report.ts +++ b/src/mol-model-props/pdbe/structure-quality-report.ts @@ -36,10 +36,50 @@ export namespace StructureQualityReport { { name: 'pdbe_structure_quality_report_issues', instance(ctx) { + const prop = get(ctx.firstModel); + if (!prop) return CifWriter.Category.Empty; + + let groupCtx: ReportExportContext; + if (ctx.cache.pdbe_structure_quality_report_issues) groupCtx = ctx.cache.pdbe_structure_quality_report_issues; + else { + const exportCtx = prop.data!.getExportContext(ctx.structures[0]); + groupCtx = createExportContext(exportCtx); + ctx.cache.pdbe_structure_quality_report_issues = groupCtx; + } + return { fields: _structure_quality_report_issues_fields, - source: ctx.structures.map(s => IndexedCustomProperty.getCifDataSource(s, StructureQualityReport.getIssueMap(s.model), ctx.cache)) - }; + source: [{ data: groupCtx, rowCount: groupCtx.elements.length }] + } + + // return { + // fields: _structure_quality_report_issues_fields, + // source: ctx.structures.map(s => IndexedCustomProperty.getCifDataSource(s, StructureQualityReport.getIssueMap(s.model), ctx.cache)) + // }; + } + }, { + name: 'pdbe_structure_quality_report_issue_types', + instance(ctx) { + const prop = get(ctx.firstModel); + if (!prop) return CifWriter.Category.Empty; + + let groupCtx: ReportExportContext; + if (ctx.cache.pdbe_structure_quality_report_issues) groupCtx = ctx.cache.pdbe_structure_quality_report_issues; + else { + const exportCtx = prop.data!.getExportContext(ctx.structures[0]); + groupCtx = createExportContext(exportCtx); + ctx.cache.pdbe_structure_quality_report_issues = groupCtx; + } + + return { + fields: _structure_quality_report_issue_types_fields, + source: [{ data: groupCtx, rowCount: groupCtx.rows.length }] + } + + // return { + // fields: _structure_quality_report_issues_fields, + // source: ctx.structures.map(s => IndexedCustomProperty.getCifDataSource(s, StructureQualityReport.getIssueMap(s.model), ctx.cache)) + // }; } }] }, @@ -58,13 +98,20 @@ export namespace StructureQualityReport { id: Column.Schema.int, ...mmCIF_residueId_schema, pdbx_PDB_model_num: Column.Schema.int, - issues: Column.Schema.List(',', x => x) + issue_group_id: Column.Schema.int + }, + pdbe_structure_quality_report_issue_types: { + group_id: Column.Schema.int, + issue_type: Column.Schema.str } } 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); + return { + residues: toTable(Schema.pdbe_structure_quality_report_issues, model.sourceData.frame.categories.pdbe_structure_quality_report_issues), + groups: toTable(Schema.pdbe_structure_quality_report_issue_types, model.sourceData.frame.categories.pdbe_structure_quality_report_issue_types), + } } export async function attachFromCifOrApi(model: Model, params: { @@ -77,7 +124,7 @@ export namespace StructureQualityReport { let info = PropertyWrapper.tryGetInfoFromCif('pdbe_structure_quality_report', model); if (info) { const data = getCifData(model); - issueMap = createIssueMapFromCif(model, data); + issueMap = createIssueMapFromCif(model, data.residues, data.groups); } else if (params.PDBe_apiSourceJson) { const data = await params.PDBe_apiSourceJson(model); if (!data) return false; @@ -112,12 +159,39 @@ export namespace StructureQualityReport { } type ExportCtx = IndexedCustomProperty.ExportCtx<string[]> -const _structure_quality_report_issues_fields: CifField<number, ExportCtx>[] = CifWriter.fields() +const _structure_quality_report_issues_fields: CifField<number, ReportExportContext>[] = CifWriter.fields<number, ReportExportContext>() .index('id') - .many(residueIdFields((i, d) => d.elements[i])) - .int('pdbx_PDB_model_num', (i, d) => P.unit.model_num(d.elements[i])) - .str('issues', (i, d) => d.property(i).join(',')) - .getFields() + .many(residueIdFields((i, d) => d.elements[i], { includeModelNum: true })) + .int('group_id', (i, d) => d.groupId[i]) + .getFields(); + +interface ReportExportContext extends ExportCtx { + groupId: number[], + rows: [number, string][] +} +const _structure_quality_report_issue_types_fields: CifField<number, ReportExportContext>[] = CifWriter.fields<number, ReportExportContext>() + .int('group_id', (i, d) => d.rows[i][0]) + .str('issue_type', (i, d) => d.rows[i][1]) + .getFields(); + +function createExportContext(ctx: ExportCtx): ReportExportContext { + const groupMap = new Map<string, number>(); + const groupId: number[] = []; + const rows: ReportExportContext['rows'] = []; + for (let i = 0; i < ctx.elements.length; i++) { + const issues = ctx.property(i); + const key = issues.join(','); + if (!groupMap.has(key)) { + const idx = groupMap.size + 1; + groupMap.set(key, idx); + for (const issue of issues) { + rows.push([idx, issue]); + } + } + groupId[i] = groupMap.get(key)!; + } + return { ...ctx, groupId, rows }; +} function createIssueMapFromJson(modelData: Model, data: any): StructureQualityReport.IssueMap | undefined { const ret = new Map<ResidueIndex, string[]>(); @@ -143,15 +217,36 @@ function createIssueMapFromJson(modelData: Model, data: any): StructureQualityRe return IndexedCustomProperty.fromResidueMap(ret); } -function createIssueMapFromCif(modelData: Model, data: Table<typeof StructureQualityReport.Schema.pdbe_structure_quality_report_issues>): StructureQualityReport.IssueMap | undefined { +function createIssueMapFromCif(modelData: Model, + residueData: Table<typeof StructureQualityReport.Schema.pdbe_structure_quality_report_issues>, + groupData: Table<typeof StructureQualityReport.Schema.pdbe_structure_quality_report_issue_types>): 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; + const { label_entity_id, label_asym_id, auth_seq_id, pdbx_PDB_ins_code, issue_group_id, pdbx_PDB_model_num, _rowCount } = residueData; + + const groups = parseIssueTypes(groupData); 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)); + ret.set(idx, groups.get(issue_group_id.value(i))!); } return IndexedCustomProperty.fromResidueMap(ret); +} + +function parseIssueTypes(groupData: Table<typeof StructureQualityReport.Schema.pdbe_structure_quality_report_issue_types>): Map<number, string[]> { + const ret = new Map<number, string[]>(); + const { group_id, issue_type } = groupData; + for (let i = 0; i < groupData._rowCount; i++) { + let group: string[]; + const id = group_id.value(i); + if (ret.has(id)) group = ret.get(id)!; + else { + group = []; + ret.set(id, group); + } + group.push(issue_type.value(i)); + } + return ret; } \ No newline at end of file diff --git a/src/mol-model/structure/export/categories/atom_site.ts b/src/mol-model/structure/export/categories/atom_site.ts index e96c104cb70e7c1941aa3429347c86ef17385e8e..8420667420e2d15aff29f3ec4500864ed2f04eca 100644 --- a/src/mol-model/structure/export/categories/atom_site.ts +++ b/src/mol-model/structure/export/categories/atom_site.ts @@ -74,9 +74,21 @@ function mappedProp<K, D>(loc: (key: K, data: D) => StructureElement, prop: (e: return (k: K, d: D) => prop(loc(k, d)); } -export function residueIdFields<K, D>(getLocation: (key: K, data: D) => StructureElement, options?: { prefix?: string, postfix?: string }): CifField<K, D>[] { +function addModelNum<K, D>(fields: CifWriter.Field.Builder<K, D>, getLocation: (key: K, data: D) => StructureElement, options?: IdFieldsOptions) { + if (options && options.includeModelNum) { + fields.int('pdbx_PDB_model_num', mappedProp(getLocation, P.unit.model_num)); + } +} + +export interface IdFieldsOptions { + prefix?: string, + postfix?: string, + includeModelNum?: boolean +} + +export function residueIdFields<K, D>(getLocation: (key: K, data: D) => StructureElement, options?: IdFieldsOptions): CifField<K, D>[] { const prefix = options && options.prefix, postfix = options && options.postfix; - return CifWriter.fields<K, D>() + const ret = CifWriter.fields<K, D>() .str(prepostfixed(prefix, postfix, `label_comp_id`), mappedProp(getLocation, P.residue.label_comp_id)) .int(prepostfixed(prefix, postfix, `label_seq_id`), mappedProp(getLocation, P.residue.label_seq_id), { encoder: E.deltaRLE, @@ -93,29 +105,35 @@ export function residueIdFields<K, D>(getLocation: (key: K, data: D) => Structur .str(prepostfixed(prefix, postfix, `auth_comp_id`), mappedProp(getLocation, P.residue.auth_comp_id)) .int(prepostfixed(prefix, postfix, `auth_seq_id`), mappedProp(getLocation, P.residue.auth_seq_id), { encoder: E.deltaRLE }) - .str(prepostfixed(prefix, postfix, `auth_asym_id`), mappedProp(getLocation, P.chain.auth_asym_id)) - .getFields(); + .str(prepostfixed(prefix, postfix, `auth_asym_id`), mappedProp(getLocation, P.chain.auth_asym_id)); + + addModelNum(ret, getLocation, options); + return ret.getFields(); } -export function chainIdFields<K, D>(getLocation: (key: K, data: D) => StructureElement, options?: { prefix?: string, postfix?: string }): CifField<K, D>[] { +export function chainIdFields<K, D>(getLocation: (key: K, data: D) => StructureElement, options?: IdFieldsOptions): CifField<K, D>[] { const prefix = options && options.prefix, postfix = options && options.postfix; - return CifField.build<K, D>() + const ret = CifField.build<K, D>() .str(prepostfixed(prefix, postfix, `label_asym_id`), mappedProp(getLocation, P.chain.label_asym_id)) .str(prepostfixed(prefix, postfix, `label_entity_id`), mappedProp(getLocation, P.chain.label_entity_id)) .str(prepostfixed(prefix, postfix, `auth_asym_id`), mappedProp(getLocation, P.chain.auth_asym_id)) - .getFields(); + + addModelNum(ret, getLocation, options); + return ret.getFields(); } -export function entityIdFields<K, D>(getLocation: (key: K, data: D) => StructureElement, options?: { prefix?: string, postfix?: string }): CifField<K, D>[] { +export function entityIdFields<K, D>(getLocation: (key: K, data: D) => StructureElement, options?: IdFieldsOptions): CifField<K, D>[] { const prefix = options && options.prefix, postfix = options && options.postfix; - return CifField.build<K, D>() + const ret = CifField.build<K, D>() .str(prepostfixed(prefix, postfix, `label_entity_id`), mappedProp(getLocation, P.chain.label_entity_id)) - .getFields(); + + addModelNum(ret, getLocation, options); + return ret.getFields(); } -export function atomIdFields<K, D>(getLocation: (key: K, data: D) => StructureElement, options?: { prefix?: string, postfix?: string }): CifField<K, D>[] { +export function atomIdFields<K, D>(getLocation: (key: K, data: D) => StructureElement, options?: IdFieldsOptions): CifField<K, D>[] { const prefix = options && options.prefix, postfix = options && options.postfix; - return CifWriter.fields<K, D>() + const ret = CifWriter.fields<K, D>() .str(prepostfixed(prefix, postfix, `label_atom_id`), mappedProp(getLocation, P.atom.label_atom_id)) .str(prepostfixed(prefix, postfix, `label_comp_id`), mappedProp(getLocation, P.residue.label_comp_id)) .int(prepostfixed(prefix, postfix, `label_seq_id`), mappedProp(getLocation, P.residue.label_seq_id), { @@ -135,6 +153,8 @@ export function atomIdFields<K, D>(getLocation: (key: K, data: D) => StructureEl .str(prepostfixed(prefix, postfix, `auth_atom_id`), mappedProp(getLocation, P.atom.auth_atom_id)) .str(prepostfixed(prefix, postfix, `auth_comp_id`), mappedProp(getLocation, P.residue.auth_comp_id)) .int(prepostfixed(prefix, postfix, `auth_seq_id`), mappedProp(getLocation, P.residue.auth_seq_id), { encoder: E.deltaRLE }) - .str(prepostfixed(prefix, postfix, `auth_asym_id`), mappedProp(getLocation, P.chain.auth_asym_id)) - .getFields(); + .str(prepostfixed(prefix, postfix, `auth_asym_id`), mappedProp(getLocation, P.chain.auth_asym_id)); + + addModelNum(ret, getLocation, options); + return ret.getFields(); } \ No newline at end of file diff --git a/src/servers/model/test.ts b/src/servers/model/test.ts index ca718fbd483bb802dcd0dbd9a8c8a0e2ca628995..7ce94b2ae4e49a43155cd6bb6fa5d4e1025a9e39 100644 --- a/src/servers/model/test.ts +++ b/src/servers/model/test.ts @@ -43,26 +43,26 @@ async function run() { try { // const testFile = '1crn.cif' const testFile = '1cbs_updated.cif' - const request = createJob({ - entryId: path.join(examplesPath, testFile), - queryName: 'full', - queryParams: { } - }); // 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: 'residueInteraction', + // queryName: 'atoms', // queryParams: { - // atom_site: { label_comp_id: 'REA' }, - // radius: 5 + // atom_site: { label_comp_id: 'ALA' } // } // }); + const request = createJob({ + entryId: path.join(examplesPath, testFile), + queryName: 'residueInteraction', + queryParams: { + atom_site: { label_comp_id: 'REA' }, + radius: 5 + } + }); const encoder = await resolveJob(request); const writer = wrapFile(path.join(outPath, testFile)); encoder.writeTo(writer);