structure-quality-report.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. /**
  2. * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author David Sehnal <david.sehnal@gmail.com>
  5. */
  6. import { Column, Table } from 'mol-data/db';
  7. import { toTable } from 'mol-io/reader/cif/schema';
  8. import { mmCIF_residueId_schema } from 'mol-io/reader/cif/schema/mmcif-extras';
  9. import { CifWriter } from 'mol-io/writer/cif';
  10. import { Model, ModelPropertyDescriptor, ResidueIndex, StructureProperties as P, Unit, IndexedCustomProperty } from 'mol-model/structure';
  11. import { residueIdFields } from 'mol-model/structure/export/categories/atom_site';
  12. import { StructureElement } from 'mol-model/structure/structure';
  13. import { CustomPropSymbol } from 'mol-script/language/symbol';
  14. import Type from 'mol-script/language/type';
  15. import { QuerySymbolRuntime } from 'mol-script/runtime/query/compiler';
  16. import { PropertyWrapper } from '../common/wrapper';
  17. import CifField = CifWriter.Field;
  18. export namespace StructureQualityReport {
  19. export type IssueMap = IndexedCustomProperty.Residue<string[]>
  20. export type Property = PropertyWrapper<IssueMap | undefined>
  21. export function get(model: Model): Property | undefined {
  22. // must be defined before the descriptor so it's not undefined.
  23. return model._dynamicPropertyData.__StructureQualityReport__;
  24. }
  25. export const Descriptor = ModelPropertyDescriptor({
  26. isStatic: false,
  27. name: 'structure_quality_report',
  28. cifExport: {
  29. prefix: 'pdbe',
  30. categories: [
  31. PropertyWrapper.defaultInfoCategory('pdbe_structure_quality_report', StructureQualityReport.get),
  32. {
  33. name: 'pdbe_structure_quality_report_issues',
  34. instance(ctx) {
  35. const prop = get(ctx.firstModel);
  36. if (!prop) return CifWriter.Category.Empty;
  37. let groupCtx: ReportExportContext;
  38. if (ctx.cache.pdbe_structure_quality_report_issues) groupCtx = ctx.cache.pdbe_structure_quality_report_issues;
  39. else {
  40. const exportCtx = prop.data!.getExportContext(ctx.structures[0]);
  41. groupCtx = createExportContext(exportCtx);
  42. ctx.cache.pdbe_structure_quality_report_issues = groupCtx;
  43. }
  44. return {
  45. fields: _structure_quality_report_issues_fields,
  46. source: [{ data: groupCtx, rowCount: groupCtx.elements.length }]
  47. }
  48. // return {
  49. // fields: _structure_quality_report_issues_fields,
  50. // source: ctx.structures.map(s => IndexedCustomProperty.getCifDataSource(s, StructureQualityReport.getIssueMap(s.model), ctx.cache))
  51. // };
  52. }
  53. }, {
  54. name: 'pdbe_structure_quality_report_issue_types',
  55. instance(ctx) {
  56. const prop = get(ctx.firstModel);
  57. if (!prop) return CifWriter.Category.Empty;
  58. let groupCtx: ReportExportContext;
  59. if (ctx.cache.pdbe_structure_quality_report_issues) groupCtx = ctx.cache.pdbe_structure_quality_report_issues;
  60. else {
  61. const exportCtx = prop.data!.getExportContext(ctx.structures[0]);
  62. groupCtx = createExportContext(exportCtx);
  63. ctx.cache.pdbe_structure_quality_report_issues = groupCtx;
  64. }
  65. return {
  66. fields: _structure_quality_report_issue_types_fields,
  67. source: [{ data: groupCtx, rowCount: groupCtx.rows.length }]
  68. }
  69. // return {
  70. // fields: _structure_quality_report_issues_fields,
  71. // source: ctx.structures.map(s => IndexedCustomProperty.getCifDataSource(s, StructureQualityReport.getIssueMap(s.model), ctx.cache))
  72. // };
  73. }
  74. }]
  75. },
  76. symbols: {
  77. issueCount: QuerySymbolRuntime.Dynamic(CustomPropSymbol('pdbe', 'structure-quality.issue-count', Type.Num),
  78. ctx => StructureQualityReport.getIssues(ctx.element).length),
  79. // TODO: add (hasIssue :: IssueType(extends string) -> boolean) symbol
  80. }
  81. });
  82. export const Schema = {
  83. pdbe_structure_quality_report: {
  84. updated_datetime_utc: Column.Schema.str
  85. },
  86. pdbe_structure_quality_report_issues: {
  87. id: Column.Schema.int,
  88. ...mmCIF_residueId_schema,
  89. pdbx_PDB_model_num: Column.Schema.int,
  90. issue_group_id: Column.Schema.int
  91. },
  92. pdbe_structure_quality_report_issue_types: {
  93. group_id: Column.Schema.int,
  94. issue_type: Column.Schema.str
  95. }
  96. }
  97. function getCifData(model: Model) {
  98. if (model.sourceData.kind !== 'mmCIF') throw new Error('Data format must be mmCIF.');
  99. return {
  100. residues: toTable(Schema.pdbe_structure_quality_report_issues, model.sourceData.frame.categories.pdbe_structure_quality_report_issues),
  101. groups: toTable(Schema.pdbe_structure_quality_report_issue_types, model.sourceData.frame.categories.pdbe_structure_quality_report_issue_types),
  102. }
  103. }
  104. export async function attachFromCifOrApi(model: Model, params: {
  105. // provide JSON from api
  106. PDBe_apiSourceJson?: (model: Model) => Promise<any>
  107. }) {
  108. if (get(model)) return true;
  109. let issueMap: IssueMap | undefined;
  110. let info = PropertyWrapper.tryGetInfoFromCif('pdbe_structure_quality_report', model);
  111. if (info) {
  112. const data = getCifData(model);
  113. issueMap = createIssueMapFromCif(model, data.residues, data.groups);
  114. } else if (params.PDBe_apiSourceJson) {
  115. const data = await params.PDBe_apiSourceJson(model);
  116. if (!data) return false;
  117. info = PropertyWrapper.createInfo();
  118. issueMap = createIssueMapFromJson(model, data);
  119. } else {
  120. return false;
  121. }
  122. model.customProperties.add(Descriptor);
  123. set(model, { info, data: issueMap });
  124. return true;
  125. }
  126. function set(model: Model, prop: Property) {
  127. (model._dynamicPropertyData.__StructureQualityReport__ as Property) = prop;
  128. }
  129. export function getIssueMap(model: Model): IssueMap | undefined {
  130. const prop = get(model);
  131. return prop && prop.data;
  132. }
  133. const _emptyArray: string[] = [];
  134. export function getIssues(e: StructureElement) {
  135. if (!Unit.isAtomic(e.unit)) return _emptyArray;
  136. const prop = StructureQualityReport.get(e.unit.model);
  137. if (!prop || !prop.data) return _emptyArray;
  138. const rI = e.unit.residueIndex[e.element];
  139. return prop.data.has(rI) ? prop.data.get(rI)! : _emptyArray;
  140. }
  141. }
  142. type ExportCtx = IndexedCustomProperty.ExportCtx<string[]>
  143. const _structure_quality_report_issues_fields: CifField<number, ReportExportContext>[] = CifWriter.fields<number, ReportExportContext>()
  144. .index('id')
  145. .many(residueIdFields((i, d) => d.elements[i], { includeModelNum: true }))
  146. .int('group_id', (i, d) => d.groupId[i])
  147. .getFields();
  148. interface ReportExportContext extends ExportCtx {
  149. groupId: number[],
  150. rows: [number, string][]
  151. }
  152. const _structure_quality_report_issue_types_fields: CifField<number, ReportExportContext>[] = CifWriter.fields<number, ReportExportContext>()
  153. .int('group_id', (i, d) => d.rows[i][0])
  154. .str('issue_type', (i, d) => d.rows[i][1])
  155. .getFields();
  156. function createExportContext(ctx: ExportCtx): ReportExportContext {
  157. const groupMap = new Map<string, number>();
  158. const groupId: number[] = [];
  159. const rows: ReportExportContext['rows'] = [];
  160. for (let i = 0; i < ctx.elements.length; i++) {
  161. const issues = ctx.property(i);
  162. const key = issues.join(',');
  163. if (!groupMap.has(key)) {
  164. const idx = groupMap.size + 1;
  165. groupMap.set(key, idx);
  166. for (const issue of issues) {
  167. rows.push([idx, issue]);
  168. }
  169. }
  170. groupId[i] = groupMap.get(key)!;
  171. }
  172. return { ...ctx, groupId, rows };
  173. }
  174. function createIssueMapFromJson(modelData: Model, data: any): StructureQualityReport.IssueMap | undefined {
  175. const ret = new Map<ResidueIndex, string[]>();
  176. if (!data.molecules) return;
  177. for (const entity of data.molecules) {
  178. const entity_id = entity.entity_id.toString();
  179. for (const chain of entity.chains) {
  180. const asym_id = chain.struct_asym_id.toString();
  181. for (const model of chain.models) {
  182. const model_id = model.model_id.toString();
  183. if (+model_id !== modelData.modelNum) continue;
  184. for (const residue of model.residues) {
  185. const auth_seq_id = residue.author_residue_number, ins_code = residue.author_insertion_code || '';
  186. const idx = modelData.atomicHierarchy.index.findResidue(entity_id, asym_id, auth_seq_id, ins_code);
  187. ret.set(idx, residue.outlier_types);
  188. }
  189. }
  190. }
  191. }
  192. return IndexedCustomProperty.fromResidueMap(ret);
  193. }
  194. function createIssueMapFromCif(modelData: Model,
  195. residueData: Table<typeof StructureQualityReport.Schema.pdbe_structure_quality_report_issues>,
  196. groupData: Table<typeof StructureQualityReport.Schema.pdbe_structure_quality_report_issue_types>): StructureQualityReport.IssueMap | undefined {
  197. const ret = new Map<ResidueIndex, string[]>();
  198. const { label_entity_id, label_asym_id, auth_seq_id, pdbx_PDB_ins_code, issue_group_id, pdbx_PDB_model_num, _rowCount } = residueData;
  199. const groups = parseIssueTypes(groupData);
  200. for (let i = 0; i < _rowCount; i++) {
  201. if (pdbx_PDB_model_num.value(i) !== modelData.modelNum) continue;
  202. 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));
  203. ret.set(idx, groups.get(issue_group_id.value(i))!);
  204. }
  205. return IndexedCustomProperty.fromResidueMap(ret);
  206. }
  207. function parseIssueTypes(groupData: Table<typeof StructureQualityReport.Schema.pdbe_structure_quality_report_issue_types>): Map<number, string[]> {
  208. const ret = new Map<number, string[]>();
  209. const { group_id, issue_type } = groupData;
  210. for (let i = 0; i < groupData._rowCount; i++) {
  211. let group: string[];
  212. const id = group_id.value(i);
  213. if (ret.has(id)) group = ret.get(id)!;
  214. else {
  215. group = [];
  216. ret.set(id, group);
  217. }
  218. group.push(issue_type.value(i));
  219. }
  220. return ret;
  221. }