superposition.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  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 { CollapsableControls, PurePluginUIComponent } from '../base';
  7. import { Icon, ArrowUpwardSvg, ArrowDownwardSvg, DeleteOutlinedSvg, HelpOutlineSvg, TuneSvg, SuperposeAtomsSvg, SuperposeChainsSvg, SuperpositionSvg } from '../controls/icons';
  8. import { Button, ToggleButton, IconButton } from '../controls/common';
  9. import { StructureElement, StructureSelection, QueryContext, Structure, StructureProperties } from '../../mol-model/structure';
  10. import { Mat4 } from '../../mol-math/linear-algebra';
  11. import { ParamDefinition as PD } from '../../mol-util/param-definition';
  12. import { StateObjectRef, StateObjectCell, StateSelection } from '../../mol-state';
  13. import { StateTransforms } from '../../mol-plugin-state/transforms';
  14. import { PluginStateObject } from '../../mol-plugin-state/objects';
  15. import { alignAndSuperpose, superpose } from '../../mol-model/structure/structure/util/superposition';
  16. import { StructureSelectionQueries } from '../../mol-plugin-state/helpers/structure-selection-query';
  17. import { structureElementStatsLabel, elementLabel } from '../../mol-theme/label';
  18. import { ParameterControls } from '../controls/parameters';
  19. import { stripTags } from '../../mol-util/string';
  20. import { StructureSelectionHistoryEntry } from '../../mol-plugin-state/manager/structure/selection';
  21. import { ToggleSelectionModeButton } from './selection';
  22. import { alignAndSuperposeWithBestDatabaseMapping } from '../../mol-model/structure/structure/util/superposition-db-mapping';
  23. import { PluginCommands } from '../../mol-plugin/commands';
  24. import { BestDatabaseSequenceMapping } from '../../mol-model-props/sequence/best-database-mapping';
  25. export class StructureSuperpositionControls extends CollapsableControls {
  26. defaultState() {
  27. return {
  28. isCollapsed: false,
  29. header: 'Superposition',
  30. brand: { accent: 'gray' as const, svg: SuperpositionSvg },
  31. isHidden: true
  32. };
  33. }
  34. componentDidMount() {
  35. this.subscribe(this.plugin.managers.structure.hierarchy.behaviors.selection, sel => {
  36. this.setState({ isHidden: sel.structures.length < 2 });
  37. });
  38. }
  39. renderControls() {
  40. return <>
  41. <SuperpositionControls />
  42. </>;
  43. }
  44. }
  45. export const StructureSuperpositionParams = {
  46. alignSequences: PD.Boolean(true, { isEssential: true, description: 'Perform a sequence alignment and use the aligned residue pairs to guide the 3D superposition.' }),
  47. };
  48. const DefaultStructureSuperpositionOptions = PD.getDefaultValues(StructureSuperpositionParams);
  49. export type StructureSuperpositionOptions = PD.ValuesFor<typeof StructureSuperpositionParams>
  50. const SuperpositionTag = 'SuperpositionTransform';
  51. type SuperpositionControlsState = {
  52. isBusy: boolean,
  53. action?: 'byChains' | 'byAtoms' | 'options',
  54. canUseDb?: boolean,
  55. options: StructureSuperpositionOptions
  56. }
  57. interface LociEntry {
  58. loci: StructureElement.Loci,
  59. label: string,
  60. cell: StateObjectCell<PluginStateObject.Molecule.Structure>
  61. };
  62. interface AtomsLociEntry extends LociEntry {
  63. atoms: StructureSelectionHistoryEntry[]
  64. };
  65. export class SuperpositionControls extends PurePluginUIComponent<{ }, SuperpositionControlsState> {
  66. state: SuperpositionControlsState = {
  67. isBusy: false,
  68. canUseDb: false,
  69. action: undefined,
  70. options: DefaultStructureSuperpositionOptions
  71. }
  72. componentDidMount() {
  73. this.subscribe(this.selection.events.changed, () => {
  74. this.forceUpdate();
  75. });
  76. this.subscribe(this.selection.events.additionsHistoryUpdated, () => {
  77. this.forceUpdate();
  78. });
  79. this.subscribe(this.plugin.behaviors.state.isBusy, v => {
  80. this.setState({ isBusy: v });
  81. });
  82. this.subscribe(this.plugin.managers.structure.hierarchy.behaviors.selection, sel => {
  83. this.setState({ canUseDb: sel.structures.every(s => !!s.cell.obj?.data && s.cell.obj.data.models.some(m => BestDatabaseSequenceMapping.Provider.isApplicable(m)) ) });
  84. });
  85. }
  86. get selection() {
  87. return this.plugin.managers.structure.selection;
  88. }
  89. async transform(s: StateObjectRef<PluginStateObject.Molecule.Structure>, matrix: Mat4) {
  90. const r = StateObjectRef.resolveAndCheck(this.plugin.state.data, s);
  91. if (!r) return;
  92. // TODO should find any TransformStructureConformation decorator instance
  93. const o = StateSelection.findTagInSubtree(this.plugin.state.data.tree, r.transform.ref, SuperpositionTag);
  94. const params = {
  95. transform: {
  96. name: 'matrix' as const,
  97. params: { data: matrix, transpose: false }
  98. }
  99. };
  100. // TODO add .insertOrUpdate to StateBuilder?
  101. const b = o
  102. ? this.plugin.state.data.build().to(o).update(params)
  103. : this.plugin.state.data.build().to(s)
  104. .insert(StateTransforms.Model.TransformStructureConformation, params, { tags: SuperpositionTag });
  105. await this.plugin.runTask(this.plugin.state.data.updateTree(b));
  106. }
  107. superposeChains = async () => {
  108. const { query } = StructureSelectionQueries.trace;
  109. const entries = this.chainEntries;
  110. const traceLocis = entries.map((e, i) => {
  111. const s = StructureElement.Loci.toStructure(e.loci);
  112. const loci = StructureSelection.toLociWithSourceUnits(query(new QueryContext(s)));
  113. return StructureElement.Loci.remap(loci, i === 0
  114. ? this.plugin.helpers.substructureParent.get(e.loci.structure.root)!.obj!.data
  115. : loci.structure.root
  116. );
  117. });
  118. const transforms = this.state.options.alignSequences
  119. ? alignAndSuperpose(traceLocis)
  120. : superpose(traceLocis);
  121. const eA = entries[0];
  122. for (let i = 1, il = traceLocis.length; i < il; ++i) {
  123. const eB = entries[i];
  124. const { bTransform, rmsd } = transforms[i - 1];
  125. await this.transform(eB.cell, bTransform);
  126. const labelA = stripTags(eA.label);
  127. const labelB = stripTags(eB.label);
  128. this.plugin.log.info(`Superposed [${labelA}] and [${labelB}] with RMSD ${rmsd.toFixed(2)}.`);
  129. }
  130. }
  131. superposeAtoms = async () => {
  132. const entries = this.atomEntries;
  133. const atomLocis = entries.map((e, i) => {
  134. return StructureElement.Loci.remap(e.loci, i === 0
  135. ? this.plugin.helpers.substructureParent.get(e.loci.structure.root)!.obj!.data
  136. : e.loci.structure.root
  137. );
  138. });
  139. const transforms = superpose(atomLocis);
  140. const eA = entries[0];
  141. for (let i = 1, il = atomLocis.length; i < il; ++i) {
  142. const eB = entries[i];
  143. const { bTransform, rmsd } = transforms[i - 1];
  144. await this.transform(eB.cell, bTransform);
  145. const labelA = stripTags(eA.label);
  146. const labelB = stripTags(eB.label);
  147. const count = entries[i].atoms.length;
  148. this.plugin.log.info(`Superposed ${count} ${count === 1 ? 'atom' : 'atoms'} of [${labelA}] and [${labelB}] with RMSD ${rmsd.toFixed(2)}.`);
  149. }
  150. }
  151. superposeDb = async () => {
  152. const input = this.plugin.managers.structure.hierarchy.behaviors.selection.value.structures;
  153. const transforms = alignAndSuperposeWithBestDatabaseMapping(input.map(s => s.cell.obj?.data!));
  154. let rmsd = 0;
  155. for (const xform of transforms) {
  156. await this.transform(input[xform.other].cell, xform.transform.bTransform);
  157. rmsd += xform.transform.rmsd;
  158. }
  159. rmsd /= transforms.length - 1;
  160. this.plugin.log.info(`Superposed ${input.length} structures with avg. RMSD ${rmsd.toFixed(2)}.`);
  161. await new Promise(res => requestAnimationFrame(res));
  162. PluginCommands.Camera.Reset(this.plugin);
  163. };
  164. toggleByChains = () => this.setState({ action: this.state.action === 'byChains' ? void 0 : 'byChains' });
  165. toggleByAtoms = () => this.setState({ action: this.state.action === 'byAtoms' ? void 0 : 'byAtoms' });
  166. toggleOptions = () => this.setState({ action: this.state.action === 'options' ? void 0 : 'options' });
  167. highlight(loci: StructureElement.Loci) {
  168. this.plugin.managers.interactivity.lociHighlights.highlightOnly({ loci }, false);
  169. }
  170. moveHistory(e: StructureSelectionHistoryEntry, direction: 'up' | 'down') {
  171. this.plugin.managers.structure.selection.modifyHistory(e, direction, void 0, true);
  172. }
  173. focusLoci(loci: StructureElement.Loci) {
  174. this.plugin.managers.camera.focusLoci(loci);
  175. }
  176. lociEntry(e: LociEntry, idx: number) {
  177. return <div className='msp-flex-row' key={idx}>
  178. <Button noOverflow title='Click to focus. Hover to highlight.' onClick={() => this.focusLoci(e.loci)} style={{ width: 'auto', textAlign: 'left' }} onMouseEnter={() => this.highlight(e.loci)} onMouseLeave={() => this.plugin.managers.interactivity.lociHighlights.clearHighlights()}>
  179. <span dangerouslySetInnerHTML={{ __html: e.label }} />
  180. </Button>
  181. </div>;
  182. }
  183. historyEntry(e: StructureSelectionHistoryEntry, idx: number) {
  184. const history = this.plugin.managers.structure.selection.additionsHistory;
  185. return <div className='msp-flex-row' key={e.id}>
  186. <Button noOverflow title='Click to focus. Hover to highlight.' onClick={() => this.focusLoci(e.loci)} style={{ width: 'auto', textAlign: 'left' }} onMouseEnter={() => this.highlight(e.loci)} onMouseLeave={() => this.plugin.managers.interactivity.lociHighlights.clearHighlights()}>
  187. {idx}. <span dangerouslySetInnerHTML={{ __html: e.label }} />
  188. </Button>
  189. {history.length > 1 && <IconButton svg={ArrowUpwardSvg} small={true} className='msp-form-control' onClick={() => this.moveHistory(e, 'up')} flex='20px' title={'Move up'} />}
  190. {history.length > 1 && <IconButton svg={ArrowDownwardSvg} small={true} className='msp-form-control' onClick={() => this.moveHistory(e, 'down')} flex='20px' title={'Move down'} />}
  191. <IconButton svg={DeleteOutlinedSvg} small={true} className='msp-form-control' onClick={() => this.plugin.managers.structure.selection.modifyHistory(e, 'remove')} flex title={'Remove'} />
  192. </div>;
  193. }
  194. atomsLociEntry(e: AtomsLociEntry, idx: number) {
  195. return <div key={idx}>
  196. <div className='msp-control-group-header'>
  197. <div className='msp-no-overflow' title={e.label}>{e.label}</div>
  198. </div>
  199. <div className='msp-control-offset'>
  200. {e.atoms.map((h, i) => this.historyEntry(h, i))}
  201. </div>
  202. </div>;
  203. }
  204. get chainEntries() {
  205. const location = StructureElement.Location.create();
  206. const entries: LociEntry[] = [];
  207. this.plugin.managers.structure.selection.entries.forEach(({ selection }, ref) => {
  208. const cell = StateObjectRef.resolveAndCheck(this.plugin.state.data, ref);
  209. if (!cell || StructureElement.Loci.isEmpty(selection)) return;
  210. // only single polymer chain selections
  211. const l = StructureElement.Loci.getFirstLocation(selection, location)!;
  212. if (selection.elements.length > 1 || StructureProperties.entity.type(l) !== 'polymer') return;
  213. const stats = StructureElement.Stats.ofLoci(selection);
  214. const counts = structureElementStatsLabel(stats, { countsOnly: true });
  215. const chain = elementLabel(l, { reverse: true, granularity: 'chain' }).split('|');
  216. const label = `${counts} | ${chain[0]} | ${chain[chain.length - 1]}`;
  217. entries.push({ loci: selection, label, cell });
  218. });
  219. return entries;
  220. }
  221. get atomEntries() {
  222. const structureEntries = new Map<Structure, StructureSelectionHistoryEntry[]>();
  223. const history = this.plugin.managers.structure.selection.additionsHistory;
  224. for (let i = 0, il = history.length; i < il; ++i) {
  225. const e = history[i];
  226. if (StructureElement.Loci.size(e.loci) !== 1) continue;
  227. const k = e.loci.structure;
  228. if (structureEntries.has(k)) structureEntries.get(k)!.push(e);
  229. else structureEntries.set(k, [e]);
  230. }
  231. const entries: AtomsLociEntry[] = [];
  232. structureEntries.forEach((atoms, structure) => {
  233. const cell = this.plugin.helpers.substructureParent.get(structure)!;
  234. const elements: StructureElement.Loci['elements'][0][] = [];
  235. for (let i = 0, il = atoms.length; i < il; ++i) {
  236. // note, we don't do loci union here to keep order of selected atoms
  237. // for atom pairing during superposition
  238. elements.push(atoms[i].loci.elements[0]);
  239. }
  240. const loci = StructureElement.Loci(atoms[0].loci.structure, elements);
  241. const label = loci.structure.label.split(' | ')[0];
  242. entries.push({ loci, label, cell, atoms });
  243. });
  244. return entries;
  245. }
  246. addByChains() {
  247. const entries = this.chainEntries;
  248. return <>
  249. {entries.length > 0 && <div className='msp-control-offset'>
  250. {entries.map((e, i) => this.lociEntry(e, i))}
  251. </div>}
  252. {entries.length < 2 && <div className='msp-control-offset msp-help-text'>
  253. <div className='msp-help-description'><Icon svg={HelpOutlineSvg} inline />Add 2 or more selections (toggle <ToggleSelectionModeButton inline /> mode) from separate structures. Selections must be limited to single polymer chains or residues therein.</div>
  254. </div>}
  255. {entries.length > 1 && <Button title='Superpose structures by selected chains.' className='msp-btn-commit msp-btn-commit-on' onClick={this.superposeChains} style={{ marginTop: '1px' }}>
  256. Superpose
  257. </Button>}
  258. </>;
  259. }
  260. addByAtoms() {
  261. const entries = this.atomEntries;
  262. return <>
  263. {entries.length > 0 && <div className='msp-control-offset'>
  264. {entries.map((e, i) => this.atomsLociEntry(e, i))}
  265. </div>}
  266. {entries.length < 2 && <div className='msp-control-offset msp-help-text'>
  267. <div className='msp-help-description'><Icon svg={HelpOutlineSvg} inline />Add 1 or more selections (toggle <ToggleSelectionModeButton inline /> mode) from
  268. separate structures. Selections must be limited to single atoms.</div>
  269. </div>}
  270. {entries.length > 1 && <Button title='Superpose structures by selected atoms.' className='msp-btn-commit msp-btn-commit-on' onClick={this.superposeAtoms} style={{ marginTop: '1px' }}>
  271. Superpose
  272. </Button>}
  273. </>;
  274. }
  275. superposeByDbMapping() {
  276. return <>
  277. <Button icon={SuperposeChainsSvg} title='Superpose structures using database mapping.' className='msp-btn msp-btn-block' onClick={this.superposeDb} style={{ marginTop: '1px' }} disabled={this.state.isBusy}>
  278. DB
  279. </Button>
  280. </>;
  281. }
  282. private setOptions = (values: StructureSuperpositionOptions) => {
  283. this.setState({ options: values });
  284. }
  285. render() {
  286. return <>
  287. <div className='msp-flex-row'>
  288. <ToggleButton icon={SuperposeChainsSvg} label='Chains' toggle={this.toggleByChains} isSelected={this.state.action === 'byChains'} disabled={this.state.isBusy} />
  289. <ToggleButton icon={SuperposeAtomsSvg} label='Atoms' toggle={this.toggleByAtoms} isSelected={this.state.action === 'byAtoms'} disabled={this.state.isBusy} />
  290. {this.state.canUseDb && this.superposeByDbMapping()}
  291. <ToggleButton icon={TuneSvg} label='' title='Options' toggle={this.toggleOptions} isSelected={this.state.action === 'options'} disabled={this.state.isBusy} style={{ flex: '0 0 40px', padding: 0 }} />
  292. </div>
  293. {this.state.action === 'byChains' && this.addByChains()}
  294. {this.state.action === 'byAtoms' && this.addByAtoms()}
  295. {this.state.action === 'options' && <div className='msp-control-offset'>
  296. <ParameterControls params={StructureSuperpositionParams} values={this.state.options} onChangeValues={this.setOptions} isDisabled={this.state.isBusy} />
  297. </div>}
  298. </>;
  299. }
  300. }