validation-report.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. /**
  2. * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  5. */
  6. import { ParamDefinition as PD } from '../../../../../mol-util/param-definition'
  7. import { PluginBehavior } from '../../../behavior';
  8. import { ValidationReport, ValidationReportProvider } from '../../../../../mol-model-props/rcsb/validation-report';
  9. import { RandomCoilIndexColorThemeProvider } from '../../../../../mol-model-props/rcsb/themes/random-coil-index';
  10. import { GeometryQualityColorThemeProvider } from '../../../../../mol-model-props/rcsb/themes/geometry-quality';
  11. import { Loci } from '../../../../../mol-model/loci';
  12. import { OrderedSet } from '../../../../../mol-data/int';
  13. import { ClashesRepresentationProvider } from '../../../../../mol-model-props/rcsb/representations/validation-report-clashes';
  14. import { DensityFitColorThemeProvider } from '../../../../../mol-model-props/rcsb/themes/density-fit';
  15. import { cantorPairing } from '../../../../../mol-data/util';
  16. import { DefaultQueryRuntimeTable } from '../../../../../mol-script/runtime/query/compiler';
  17. import { StructureSelectionQuery, StructureSelectionCategory } from '../../../../../mol-plugin-state/helpers/structure-selection-query';
  18. import { MolScriptBuilder as MS } from '../../../../../mol-script/language/builder';
  19. import { Task } from '../../../../../mol-task';
  20. import { StructureRepresentationPresetProvider, PresetStructureRepresentations } from '../../../../../mol-plugin-state/builder/structure/representation-preset';
  21. import { StateObjectRef } from '../../../../../mol-state';
  22. import { Model } from '../../../../../mol-model/structure';
  23. export const RCSBValidationReport = PluginBehavior.create<{ autoAttach: boolean, showTooltip: boolean }>({
  24. name: 'rcsb-validation-report-prop',
  25. category: 'custom-props',
  26. display: {
  27. name: 'Validation Report',
  28. description: 'Data from wwPDB Validation Report, obtained via RCSB PDB.'
  29. },
  30. ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean, showTooltip: boolean }> {
  31. private provider = ValidationReportProvider
  32. private labelProvider = {
  33. label: (loci: Loci): string | undefined => {
  34. if (!this.params.showTooltip) return
  35. return [
  36. geometryQualityLabel(loci),
  37. densityFitLabel(loci),
  38. randomCoilIndexLabel(loci)
  39. ].filter(l => !!l).join('</br>')
  40. }
  41. }
  42. register(): void {
  43. DefaultQueryRuntimeTable.addCustomProp(this.provider.descriptor);
  44. this.ctx.customModelProperties.register(this.provider, this.params.autoAttach);
  45. this.ctx.managers.lociLabels.addProvider(this.labelProvider);
  46. this.ctx.representation.structure.themes.colorThemeRegistry.add(DensityFitColorThemeProvider)
  47. this.ctx.representation.structure.themes.colorThemeRegistry.add(GeometryQualityColorThemeProvider)
  48. this.ctx.representation.structure.themes.colorThemeRegistry.add(RandomCoilIndexColorThemeProvider)
  49. this.ctx.representation.structure.registry.add(ClashesRepresentationProvider)
  50. this.ctx.query.structure.registry.add(hasClash)
  51. this.ctx.builders.structure.representation.registerPreset(ValidationReportGeometryQualityPreset)
  52. this.ctx.builders.structure.representation.registerPreset(ValidationReportDensityFitPreset)
  53. this.ctx.builders.structure.representation.registerPreset(ValidationReportRandomCoilIndexPreset)
  54. }
  55. update(p: { autoAttach: boolean, showTooltip: boolean }) {
  56. let updated = this.params.autoAttach !== p.autoAttach
  57. this.params.autoAttach = p.autoAttach;
  58. this.params.showTooltip = p.showTooltip;
  59. this.ctx.customStructureProperties.setDefaultAutoAttach(this.provider.descriptor.name, this.params.autoAttach);
  60. return updated;
  61. }
  62. unregister() {
  63. // TODO
  64. // DefaultQueryRuntimeTable.removeCustomProp(this.provider.descriptor);
  65. this.ctx.customStructureProperties.unregister(this.provider.descriptor.name);
  66. this.ctx.managers.lociLabels.removeProvider(this.labelProvider);
  67. this.ctx.representation.structure.themes.colorThemeRegistry.remove(DensityFitColorThemeProvider)
  68. this.ctx.representation.structure.themes.colorThemeRegistry.remove(GeometryQualityColorThemeProvider)
  69. this.ctx.representation.structure.themes.colorThemeRegistry.remove(RandomCoilIndexColorThemeProvider)
  70. this.ctx.representation.structure.registry.remove(ClashesRepresentationProvider)
  71. this.ctx.query.structure.registry.remove(hasClash)
  72. this.ctx.builders.structure.representation.unregisterPreset(ValidationReportGeometryQualityPreset)
  73. this.ctx.builders.structure.representation.unregisterPreset(ValidationReportDensityFitPreset)
  74. this.ctx.builders.structure.representation.unregisterPreset(ValidationReportRandomCoilIndexPreset)
  75. }
  76. },
  77. params: () => ({
  78. autoAttach: PD.Boolean(false),
  79. showTooltip: PD.Boolean(true),
  80. baseUrl: PD.Text(ValidationReport.DefaultBaseUrl)
  81. })
  82. });
  83. //
  84. function geometryQualityLabel(loci: Loci): string | undefined {
  85. if (loci.kind === 'element-loci') {
  86. if (loci.elements.length === 0) return
  87. if (loci.elements.length === 1 && OrderedSet.size(loci.elements[0].indices) === 1) {
  88. const { unit, indices } = loci.elements[0]
  89. const validationReport = ValidationReportProvider.get(unit.model).value
  90. if (!validationReport) return
  91. if (!unit.model.customProperties.hasReference(ValidationReportProvider.descriptor)) return
  92. const { bondOutliers, angleOutliers } = validationReport
  93. const eI = unit.elements[OrderedSet.start(indices)]
  94. const issues = new Set<string>()
  95. const bonds = bondOutliers.index.get(eI)
  96. if (bonds) bonds.forEach(b => issues.add(bondOutliers.data[b].tag))
  97. const angles = angleOutliers.index.get(eI)
  98. if (angles) angles.forEach(a => issues.add(angleOutliers.data[a].tag))
  99. if (issues.size === 0) {
  100. return `Geometry Quality <small>(1 Atom)</small>: no issues`;
  101. }
  102. const summary: string[] = []
  103. issues.forEach(name => summary.push(name))
  104. return `Geometry Quality <small>(1 Atom)</small>: ${summary.join(', ')}`;
  105. }
  106. let hasValidationReport = false
  107. const seen = new Set<number>()
  108. const cummulativeIssues = new Map<string, number>()
  109. for (const { indices, unit } of loci.elements) {
  110. const validationReport = ValidationReportProvider.get(unit.model).value
  111. if (!validationReport) continue
  112. if (!unit.model.customProperties.hasReference(ValidationReportProvider.descriptor)) continue
  113. hasValidationReport = true
  114. const { geometryIssues } = validationReport
  115. const residueIndex = unit.model.atomicHierarchy.residueAtomSegments.index
  116. const { elements } = unit
  117. OrderedSet.forEach(indices, idx => {
  118. const eI = elements[idx]
  119. const rI = residueIndex[eI]
  120. const residueKey = cantorPairing(rI, unit.id)
  121. if (!seen.has(residueKey)) {
  122. const issues = geometryIssues.get(rI)
  123. if (issues) {
  124. issues.forEach(name => {
  125. const count = cummulativeIssues.get(name) || 0
  126. cummulativeIssues.set(name, count + 1)
  127. })
  128. }
  129. seen.add(residueKey)
  130. }
  131. })
  132. }
  133. if (!hasValidationReport) return
  134. const residueCount = `<small>(${seen.size} ${seen.size > 1 ? 'Residues' : 'Residue'})</small>`
  135. if (cummulativeIssues.size === 0) {
  136. return `Geometry Quality ${residueCount}: no issues`;
  137. }
  138. const summary: string[] = []
  139. cummulativeIssues.forEach((count, name) => {
  140. summary.push(`${name}${count > 1 ? ` \u00D7 ${count}` : ''}`)
  141. })
  142. return `Geometry Quality ${residueCount}: ${summary.join(', ')}`;
  143. }
  144. }
  145. function densityFitLabel(loci: Loci): string | undefined {
  146. if (loci.kind === 'element-loci') {
  147. if (loci.elements.length === 0) return;
  148. const seen = new Set<number>()
  149. const rsrzSeen = new Set<number>()
  150. const rsccSeen = new Set<number>()
  151. let rsrzSum = 0
  152. let rsccSum = 0
  153. for (const { indices, unit } of loci.elements) {
  154. const validationReport = ValidationReportProvider.get(unit.model).value
  155. if (!validationReport) continue
  156. if (!unit.model.customProperties.hasReference(ValidationReportProvider.descriptor)) continue
  157. const { rsrz, rscc } = validationReport
  158. const residueIndex = unit.model.atomicHierarchy.residueAtomSegments.index
  159. const { elements } = unit
  160. OrderedSet.forEach(indices, idx => {
  161. const eI = elements[idx]
  162. const rI = residueIndex[eI]
  163. const residueKey = cantorPairing(rI, unit.id)
  164. if (!seen.has(residueKey)) {
  165. const rsrzValue = rsrz.get(rI)
  166. const rsccValue = rscc.get(rI)
  167. if (rsrzValue !== undefined) {
  168. rsrzSum += rsrzValue
  169. rsrzSeen.add(residueKey)
  170. } else if (rsccValue !== undefined) {
  171. rsccSum += rsccValue
  172. rsccSeen.add(residueKey)
  173. }
  174. seen.add(residueKey)
  175. }
  176. })
  177. }
  178. if (seen.size === 0) return
  179. const summary: string[] = []
  180. if (rsrzSeen.size) {
  181. const rsrzCount = `<small>(${rsrzSeen.size} ${rsrzSeen.size > 1 ? 'Residues avg.' : 'Residue'})</small>`
  182. const rsrzAvg = rsrzSum / rsrzSeen.size
  183. summary.push(`Real Space R ${rsrzCount}: ${rsrzAvg.toFixed(2)}`)
  184. }
  185. if (rsccSeen.size) {
  186. const rsccCount = `<small>(${rsccSeen.size} ${rsccSeen.size > 1 ? 'Residues avg.' : 'Residue'})</small>`
  187. const rsccAvg = rsccSum / rsccSeen.size
  188. summary.push(`Real Space Correlation Coefficient ${rsccCount}: ${rsccAvg.toFixed(2)}`)
  189. }
  190. if (summary.length) {
  191. return summary.join('</br>')
  192. }
  193. }
  194. }
  195. function randomCoilIndexLabel(loci: Loci): string | undefined {
  196. if (loci.kind === 'element-loci') {
  197. if (loci.elements.length === 0) return;
  198. const seen = new Set<number>()
  199. let sum = 0
  200. for (const { indices, unit } of loci.elements) {
  201. const validationReport = ValidationReportProvider.get(unit.model).value
  202. if (!validationReport) continue
  203. if (!unit.model.customProperties.hasReference(ValidationReportProvider.descriptor)) continue
  204. const { rci } = validationReport
  205. const residueIndex = unit.model.atomicHierarchy.residueAtomSegments.index
  206. const { elements } = unit
  207. OrderedSet.forEach(indices, idx => {
  208. const eI = elements[idx]
  209. const rI = residueIndex[eI]
  210. const residueKey = cantorPairing(rI, unit.id)
  211. if (!seen.has(residueKey)) {
  212. const rciValue = rci.get(rI)
  213. if (rciValue !== undefined) {
  214. sum += rciValue
  215. seen.add(residueKey)
  216. }
  217. }
  218. })
  219. }
  220. if (seen.size === 0) return
  221. const residueCount = `<small>(${seen.size} ${seen.size > 1 ? 'Residues avg.' : 'Residue'})</small>`
  222. const rciAvg = sum / seen.size
  223. return `Random Coil Index ${residueCount}: ${rciAvg.toFixed(2)}`
  224. }
  225. }
  226. //
  227. const hasClash = StructureSelectionQuery('Residues with Clashes', MS.struct.modifier.union([
  228. MS.struct.modifier.wholeResidues([
  229. MS.struct.modifier.union([
  230. MS.struct.generator.atomGroups({
  231. 'chain-test': MS.core.rel.eq([MS.ammp('objectPrimitive'), 'atomistic']),
  232. 'atom-test': ValidationReport.symbols.hasClash.symbol(),
  233. })
  234. ])
  235. ])
  236. ]), {
  237. description: 'Select residues with clashes in the wwPDB validation report.',
  238. category: StructureSelectionCategory.Residue,
  239. ensureCustomProperties: (ctx, structure) => {
  240. return ValidationReportProvider.attach(ctx, structure.models[0])
  241. }
  242. })
  243. //
  244. export const ValidationReportGeometryQualityPreset = StructureRepresentationPresetProvider({
  245. id: 'preset-structure-representation-rcsb-validation-report-geometry-uality',
  246. display: {
  247. name: 'Validation Report (Geometry Quality)', group: 'Annotation',
  248. description: 'Color structure based on geometry quality; show geometry clashes. Data from wwPDB Validation Report, obtained via RCSB PDB.'
  249. },
  250. isApplicable(a) {
  251. return a.data.models.length === 1 && ValidationReport.isApplicable(a.data.models[0])
  252. },
  253. params: () => StructureRepresentationPresetProvider.CommonParams,
  254. async apply(ref, params, plugin) {
  255. const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
  256. const model = structureCell?.obj?.data.model
  257. if (!structureCell || !model) return {};
  258. await plugin.runTask(Task.create('Validation Report', async runtime => {
  259. await ValidationReportProvider.attach({ fetch: plugin.fetch, runtime }, model)
  260. }))
  261. const colorTheme = GeometryQualityColorThemeProvider.name as any
  262. const { components, representations } = await PresetStructureRepresentations.auto.apply(ref, { ...params, globalThemeName: colorTheme }, plugin)
  263. const clashes = await plugin.builders.structure.tryCreateComponentFromExpression(structureCell, hasClash.expression, 'clashes', { label: 'Clashes' })
  264. const { update, builder, typeParams, color } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
  265. let clashesBallAndStick, clashesSnfg3d;
  266. if (representations) {
  267. clashesBallAndStick = builder.buildRepresentation(update, clashes, { type: 'ball-and-stick', typeParams, color: colorTheme }, { tag: 'clashes-ball-and-stick' });
  268. clashesSnfg3d = builder.buildRepresentation<any>(update, clashes, { type: ClashesRepresentationProvider.name, typeParams, color }, { tag: 'clashes-snfg-3d' });
  269. }
  270. await update.commit({ revertOnError: true });
  271. return { components: { ...components, clashes }, representations: { ...representations, clashesBallAndStick, clashesSnfg3d } };
  272. }
  273. });
  274. export const ValidationReportDensityFitPreset = StructureRepresentationPresetProvider({
  275. id: 'preset-structure-representation-rcsb-validation-report-density-fit',
  276. display: {
  277. name: 'Validation Report (Density Fit)', group: 'Annotation',
  278. description: 'Color structure based on density fit. Data from wwPDB Validation Report, obtained via RCSB PDB.'
  279. },
  280. isApplicable(a) {
  281. return a.data.models.length === 1 && ValidationReport.isApplicable(a.data.models[0]) && Model.hasXrayMap(a.data.models[0])
  282. },
  283. params: () => StructureRepresentationPresetProvider.CommonParams,
  284. async apply(ref, params, plugin) {
  285. const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
  286. const model = structureCell?.obj?.data.model
  287. if (!structureCell || !model) return {};
  288. await plugin.runTask(Task.create('Validation Report', async runtime => {
  289. await ValidationReportProvider.attach({ fetch: plugin.fetch, runtime }, model)
  290. }))
  291. const colorTheme = DensityFitColorThemeProvider.name as any
  292. return await PresetStructureRepresentations.auto.apply(ref, { ...params, globalThemeName: colorTheme }, plugin)
  293. }
  294. });
  295. export const ValidationReportRandomCoilIndexPreset = StructureRepresentationPresetProvider({
  296. id: 'preset-structure-representation-rcsb-validation-report-random-coil-index',
  297. display: {
  298. name: 'Validation Report (Random Coil Index)', group: 'Annotation',
  299. description: 'Color structure based on Random Coil Index. Data from wwPDB Validation Report, obtained via RCSB PDB.'
  300. },
  301. isApplicable(a) {
  302. return a.data.models.length === 1 && ValidationReport.isApplicable(a.data.models[0]) && Model.isFromNmr(a.data.models[0])
  303. },
  304. params: () => StructureRepresentationPresetProvider.CommonParams,
  305. async apply(ref, params, plugin) {
  306. const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
  307. const model = structureCell?.obj?.data.model
  308. if (!structureCell || !model) return {};
  309. await plugin.runTask(Task.create('Validation Report', async runtime => {
  310. await ValidationReportProvider.attach({ fetch: plugin.fetch, runtime }, model)
  311. }))
  312. const colorTheme = RandomCoilIndexColorThemeProvider.name as any
  313. return await PresetStructureRepresentations.auto.apply(ref, { ...params, globalThemeName: colorTheme }, plugin)
  314. }
  315. });