prop.ts 12 KB

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