behavior.ts 17 KB

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