prop.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. /**
  2. * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author David Sehnal <david.sehnal@gmail.com>
  5. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  6. */
  7. import { Column, Table } from '../../../mol-data/db';
  8. import { toTable } from '../../../mol-io/reader/cif/schema';
  9. import { mmCIF_residueId_schema } from '../../../mol-io/reader/cif/schema/mmcif-extras';
  10. import { CifWriter } from '../../../mol-io/writer/cif';
  11. import { Model, ResidueIndex, Unit, IndexedCustomProperty } from '../../../mol-model/structure';
  12. import { residueIdFields } from '../../../mol-model/structure/export/categories/atom_site';
  13. import { StructureElement, CifExportContext, Structure } from '../../../mol-model/structure/structure';
  14. import { CustomPropSymbol } from '../../../mol-script/language/symbol';
  15. import { Type } from '../../../mol-script/language/type';
  16. import { QuerySymbolRuntime } from '../../../mol-script/runtime/query/compiler';
  17. import { ParamDefinition as PD } from '../../../mol-util/param-definition';
  18. import { arraySetAdd } from '../../../mol-util/array';
  19. import { MmcifFormat } from '../../../mol-model-formats/structure/mmcif';
  20. import { PropertyWrapper } from '../../../mol-model-props/common/wrapper';
  21. import { CustomProperty } from '../../../mol-model-props/common/custom-property';
  22. import { CustomModelProperty } from '../../../mol-model-props/common/custom-model-property';
  23. import { Asset } from '../../../mol-util/assets';
  24. import { CustomPropertyDescriptor } from '../../../mol-model/custom-property';
  25. export { StructureQualityReport };
  26. type StructureQualityReport = PropertyWrapper<{
  27. issues: IndexedCustomProperty.Residue<string[]>,
  28. issueTypes: string[]
  29. } | undefined>
  30. namespace StructureQualityReport {
  31. export const DefaultServerUrl = 'https://www.ebi.ac.uk/pdbe/api/validation/residuewise_outlier_summary/entry/';
  32. export function getEntryUrl(pdbId: string, serverUrl: string) {
  33. return `${serverUrl}/${pdbId.toLowerCase()}`;
  34. }
  35. export function isApplicable(model?: Model): boolean {
  36. return !!model && Model.hasPdbId(model);
  37. }
  38. export const Schema = {
  39. pdbe_structure_quality_report: {
  40. updated_datetime_utc: Column.Schema.str
  41. },
  42. pdbe_structure_quality_report_issues: {
  43. id: Column.Schema.int,
  44. ...mmCIF_residueId_schema,
  45. pdbx_PDB_model_num: Column.Schema.int,
  46. issue_type_group_id: Column.Schema.int
  47. },
  48. pdbe_structure_quality_report_issue_types: {
  49. group_id: Column.Schema.int,
  50. issue_type: Column.Schema.str
  51. }
  52. };
  53. export type Schema = typeof Schema
  54. export function fromJson(model: Model, data: any) {
  55. const info = PropertyWrapper.createInfo();
  56. const issueMap = createIssueMapFromJson(model, data);
  57. return { info, data: issueMap };
  58. }
  59. export async function fromServer(ctx: CustomProperty.Context, model: Model, props: StructureQualityReportProps): Promise<CustomProperty.Data<StructureQualityReport>> {
  60. const url = Asset.getUrlAsset(ctx.assetManager, getEntryUrl(model.entryId, props.serverUrl));
  61. const json = await ctx.assetManager.resolve(url, 'json').runInContext(ctx.runtime);
  62. const data = json.data[model.entryId.toLowerCase()];
  63. if (!data) throw new Error('missing data');
  64. return { value: fromJson(model, data), assets: [json] };
  65. }
  66. export function fromCif(ctx: CustomProperty.Context, model: Model, props: StructureQualityReportProps): StructureQualityReport | undefined {
  67. const info = PropertyWrapper.tryGetInfoFromCif('pdbe_structure_quality_report', model);
  68. if (!info) return;
  69. const data = getCifData(model);
  70. const issueMap = createIssueMapFromCif(model, data.residues, data.groups);
  71. return { info, data: issueMap };
  72. }
  73. export async function fromCifOrServer(ctx: CustomProperty.Context, model: Model, props: StructureQualityReportProps): Promise<CustomProperty.Data<StructureQualityReport>> {
  74. const cif = fromCif(ctx, model, props);
  75. return cif ? { value: cif } : fromServer(ctx, model, props);
  76. }
  77. const _emptyArray: string[] = [];
  78. export function getIssues(e: StructureElement.Location) {
  79. if (!Unit.isAtomic(e.unit)) return _emptyArray;
  80. const prop = StructureQualityReportProvider.get(e.unit.model).value;
  81. if (!prop || !prop.data) return _emptyArray;
  82. const rI = e.unit.residueIndex[e.element];
  83. return prop.data.issues.has(rI) ? prop.data.issues.get(rI)! : _emptyArray;
  84. }
  85. export function getIssueTypes(structure?: Structure) {
  86. if (!structure) return _emptyArray;
  87. const prop = StructureQualityReportProvider.get(structure.models[0]).value;
  88. if (!prop || !prop.data) return _emptyArray;
  89. return prop.data.issueTypes;
  90. }
  91. function getCifData(model: Model) {
  92. if (!MmcifFormat.is(model.sourceData)) throw new Error('Data format must be mmCIF.');
  93. return {
  94. residues: toTable(Schema.pdbe_structure_quality_report_issues, model.sourceData.data.frame.categories.pdbe_structure_quality_report_issues),
  95. groups: toTable(Schema.pdbe_structure_quality_report_issue_types, model.sourceData.data.frame.categories.pdbe_structure_quality_report_issue_types),
  96. };
  97. }
  98. }
  99. export const StructureQualityReportParams = {
  100. serverUrl: PD.Text(StructureQualityReport.DefaultServerUrl, { description: 'JSON API Server URL' })
  101. };
  102. export type StructureQualityReportParams = typeof StructureQualityReportParams
  103. export type StructureQualityReportProps = PD.Values<StructureQualityReportParams>
  104. export const StructureQualityReportProvider: CustomModelProperty.Provider<StructureQualityReportParams, StructureQualityReport> = CustomModelProperty.createProvider({
  105. label: 'Structure Quality Report',
  106. descriptor: CustomPropertyDescriptor<ReportExportContext, any>({
  107. name: 'pdbe_structure_quality_report',
  108. cifExport: {
  109. prefix: 'pdbe',
  110. context(ctx: CifExportContext) {
  111. return createExportContext(ctx);
  112. },
  113. categories: [
  114. PropertyWrapper.defaultInfoCategory<ReportExportContext>('pdbe_structure_quality_report', ctx => ctx.info),
  115. {
  116. name: 'pdbe_structure_quality_report_issues',
  117. instance(ctx: ReportExportContext) {
  118. return {
  119. fields: _structure_quality_report_issues_fields,
  120. source: ctx.models.map(data => ({ data, rowCount: data.elements.length }))
  121. };
  122. }
  123. }, {
  124. name: 'pdbe_structure_quality_report_issue_types',
  125. instance(ctx: ReportExportContext) {
  126. return CifWriter.Category.ofTable(ctx.issueTypes);
  127. }
  128. }]
  129. },
  130. symbols: {
  131. issueCount: QuerySymbolRuntime.Dynamic(CustomPropSymbol('pdbe', 'structure-quality.issue-count', Type.Num),
  132. ctx => StructureQualityReport.getIssues(ctx.element).length),
  133. // TODO: add (hasIssue :: IssueType(extends string) -> boolean) symbol
  134. }
  135. }),
  136. type: 'static',
  137. defaultParams: StructureQualityReportParams,
  138. getParams: (data: Model) => StructureQualityReportParams,
  139. isApplicable: (data: Model) => StructureQualityReport.isApplicable(data),
  140. obtain: async (ctx: CustomProperty.Context, data: Model, props: Partial<StructureQualityReportProps>) => {
  141. const p = { ...PD.getDefaultValues(StructureQualityReportParams), ...props };
  142. return await StructureQualityReport.fromCifOrServer(ctx, data, p);
  143. }
  144. });
  145. const _structure_quality_report_issues_fields = CifWriter.fields<number, ReportExportContext['models'][0]>()
  146. .index('id')
  147. .many(residueIdFields((i, d) => d.elements[i], { includeModelNum: true }))
  148. .int('issue_type_group_id', (i, d) => d.groupId[i])
  149. .getFields();
  150. interface ReportExportContext {
  151. models: {
  152. elements: StructureElement.Location[],
  153. groupId: number[]
  154. }[],
  155. info: PropertyWrapper.Info,
  156. issueTypes: Table<StructureQualityReport.Schema['pdbe_structure_quality_report_issue_types']>,
  157. }
  158. function createExportContext(ctx: CifExportContext): ReportExportContext {
  159. const groupMap = new Map<string, number>();
  160. const models: ReportExportContext['models'] = [];
  161. const group_id: number[] = [], issue_type: string[] = [];
  162. let info: PropertyWrapper.Info = PropertyWrapper.createInfo();
  163. for (const s of ctx.structures) {
  164. const prop = StructureQualityReportProvider.get(s.model).value;
  165. if (prop) info = prop.info;
  166. if (!prop || !prop.data) continue;
  167. const { elements, property } = prop.data.issues.getElements(s);
  168. if (elements.length === 0) continue;
  169. const elementGroupId: number[] = [];
  170. for (let i = 0; i < elements.length; i++) {
  171. const issues = property(i);
  172. const key = issues.join(',');
  173. if (!groupMap.has(key)) {
  174. const idx = groupMap.size + 1;
  175. groupMap.set(key, idx);
  176. for (const issue of issues) {
  177. group_id.push(idx);
  178. issue_type.push(issue);
  179. }
  180. }
  181. elementGroupId[i] = groupMap.get(key)!;
  182. }
  183. models.push({ elements, groupId: elementGroupId });
  184. }
  185. return {
  186. info,
  187. models,
  188. issueTypes: Table.ofArrays(StructureQualityReport.Schema.pdbe_structure_quality_report_issue_types, { group_id, issue_type })
  189. };
  190. }
  191. function createIssueMapFromJson(modelData: Model, data: any): StructureQualityReport['data'] | undefined {
  192. const ret = new Map<ResidueIndex, string[]>();
  193. if (!data.molecules) return;
  194. const issueTypes: string[] = [];
  195. for (const entity of data.molecules) {
  196. const entity_id = entity.entity_id.toString();
  197. for (const chain of entity.chains) {
  198. const asym_id = chain.struct_asym_id.toString();
  199. for (const model of chain.models) {
  200. const model_id = model.model_id.toString();
  201. if (+model_id !== modelData.modelNum) continue;
  202. for (const residue of model.residues) {
  203. const auth_seq_id = residue.author_residue_number, ins_code = residue.author_insertion_code || '';
  204. const idx = modelData.atomicHierarchy.index.findResidue(entity_id, asym_id, auth_seq_id, ins_code);
  205. ret.set(idx, residue.outlier_types);
  206. for (const t of residue.outlier_types) {
  207. arraySetAdd(issueTypes, t);
  208. }
  209. }
  210. }
  211. }
  212. }
  213. return {
  214. issues: IndexedCustomProperty.fromResidueMap(ret),
  215. issueTypes
  216. };
  217. }
  218. function createIssueMapFromCif(modelData: Model,
  219. residueData: Table<typeof StructureQualityReport.Schema.pdbe_structure_quality_report_issues>,
  220. groupData: Table<typeof StructureQualityReport.Schema.pdbe_structure_quality_report_issue_types>): StructureQualityReport['data'] | undefined {
  221. const ret = new Map<ResidueIndex, string[]>();
  222. const { label_entity_id, label_asym_id, auth_seq_id, pdbx_PDB_ins_code, issue_type_group_id, pdbx_PDB_model_num, _rowCount } = residueData;
  223. const groups = parseIssueTypes(groupData);
  224. for (let i = 0; i < _rowCount; i++) {
  225. if (pdbx_PDB_model_num.value(i) !== modelData.modelNum) continue;
  226. 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));
  227. ret.set(idx, groups.get(issue_type_group_id.value(i))!);
  228. }
  229. const issueTypes: string[] = [];
  230. groups.forEach(issues => {
  231. for (const t of issues) {
  232. arraySetAdd(issueTypes, t);
  233. }
  234. });
  235. return {
  236. issues: IndexedCustomProperty.fromResidueMap(ret),
  237. issueTypes
  238. };
  239. }
  240. function parseIssueTypes(groupData: Table<typeof StructureQualityReport.Schema.pdbe_structure_quality_report_issue_types>): Map<number, string[]> {
  241. const ret = new Map<number, string[]>();
  242. const { group_id, issue_type } = groupData;
  243. for (let i = 0; i < groupData._rowCount; i++) {
  244. let group: string[];
  245. const id = group_id.value(i);
  246. if (ret.has(id)) group = ret.get(id)!;
  247. else {
  248. group = [];
  249. ret.set(id, group);
  250. }
  251. group.push(issue_type.value(i));
  252. }
  253. return ret;
  254. }