Browse Source

move pecos-related code into rcsb-molstar

JonStargaryen 3 years ago
parent
commit
34afdaed7a
4 changed files with 154 additions and 36 deletions
  1. 4 0
      CHANGELOG.md
  2. 105 0
      src/viewer/helpers/superpose/pecos-integration.ts
  3. 22 36
      src/viewer/index.html
  4. 23 0
      src/viewer/index.ts

+ 4 - 0
CHANGELOG.md

@@ -2,6 +2,10 @@
 
 [Semantic Versioning](https://semver.org/)
 
+## [1.8.0] - 2021-07-13
+### Added
+- Moved code for motif alignment (i.e., talking to pecos) here
+
 ## [1.7.4] - 2021-07-12
 ### Bug fixes
 - structure selection: handle selection of full chains

+ 105 - 0
src/viewer/helpers/superpose/pecos-integration.ts

@@ -0,0 +1,105 @@
+import { Mat4 } from 'molstar/lib/mol-math/linear-algebra';
+
+const ALIGNMENT_URL = 'https://alignment.rcsb.org/api/v1-beta/';
+
+// TODO probably move to selection.ts
+export type Residue = { label_asym_id: string, struct_oper_id?: string, label_seq_id: number };
+export type MotifSelection = { entry_id: string, residue_ids: Residue[] }
+export type MotifAlignmentRequest = {
+    query: {
+        entry_id: string,
+        residue_ids: Residue[]
+    },
+    hits: ({
+        id: string,
+        assembly_id: string,
+    } & MotifSelection)[]
+}
+
+export async function alignMotifs(query: MotifSelection, hit: MotifSelection): Promise<{ rmsd: number, matrix: Mat4 }> {
+    const q = {
+        mode: 'pairwise',
+        method: {
+            name: 'qcp',
+            parameters: {
+                atom_pairing_strategy: 'all'
+            }
+        },
+        structures: [
+            {
+                entry_id: query.entry_id,
+                residue_ids: convertToPecosIdentifiers(query.residue_ids)
+            },
+            {
+                entry_id: hit.entry_id,
+                residue_ids: convertToPecosIdentifiers(hit.residue_ids)
+            }
+        ]
+    };
+
+    const formData = new FormData();
+    formData.append('query', JSON.stringify(q));
+
+    const r = await fetch(ALIGNMENT_URL + 'structures/submit', { method: 'POST', body: formData });
+
+    if (r.status !== 200) {
+        throw new Error('Failed to submit the job');
+    }
+
+    const uuid = await r.text();
+    const url = ALIGNMENT_URL + 'structures/results?uuid=' + uuid;
+    // polls every 25ms for up to 10 seconds
+    const result = await pollUntilDone(url, 25, 10 * 1000);
+
+    const { alignment_summary, rigid_blocks } = result.results[0];
+    return { rmsd: alignment_summary.scores[0].value, matrix: rigid_blocks[0].transformations[0] };
+}
+
+// convert strucmotif/arches residue identifiers to the pecos/sierra flavor
+function convertToPecosIdentifiers(identifiers: Residue[]) {
+    return identifiers.map(i => {
+        const o = Object.create(null);
+        Object.assign(o, {
+            asym_id: i.label_asym_id,
+            seq_id: i.label_seq_id
+        });
+        if (i.struct_oper_id) Object.assign(o, { struct_oper_id: i.struct_oper_id });
+        return o;
+    });
+}
+
+// create a promise that resolves after a short delay
+function delay(t: number) {
+    return new Promise(function(resolve) {
+        setTimeout(resolve, t);
+    });
+}
+
+/**
+ * Poll until results are available.
+ * @param url is the URL to request
+ * @param interval is how often to poll
+ * @param timeout is how long to poll waiting for a result (0 means try forever)
+ */
+async function pollUntilDone(url: string, interval: number, timeout: number) {
+    const start = Date.now();
+    async function run(): Promise<any> {
+        const r = await fetch(url);
+        const data = await r.json();
+        if (data.info.status === 'COMPLETE') {
+            // we know we're done here, return from here whatever you
+            // want the final resolved value of the promise to be
+            return data;
+        } else if (data.info.status === 'ERROR') {
+            throw new Error('Failed to complete the job. Error: ' + data.info.message);
+        } else {
+            if (timeout !== 0 && Date.now() - start > timeout) {
+                throw new Error('timeout error on pollUntilDone');
+            } else {
+                // run again with a short delay
+                return delay(interval).then(run);
+            }
+        }
+    }
+    return run();
+}

+ 22 - 36
src/viewer/index.html

@@ -421,44 +421,30 @@
             }
 
             function motifs1() {
+                const payload = {
+                    query: {
+                        entry_id: '1G2F',
+                        residue_ids: [
+                            { label_asym_id: 'F', struct_oper_id: '1', label_seq_id: 7 },
+                            { label_asym_id: 'F', struct_oper_id: '1', label_seq_id: 25 },
+                            { label_asym_id: 'F', struct_oper_id: '1', label_seq_id: 29 }
+                        ]
+                    },
+                    hits: [{
+                        assembly_id: '1',
+                        entry_id: '5WJQ',
+                        id: '1',
+                        residue_ids: [
+                            { label_asym_id: 'C', struct_oper_id: '1', label_seq_id: 95 },
+                            { label_asym_id: 'C', struct_oper_id: '1', label_seq_id: 111 },
+                            { label_asym_id: 'C', struct_oper_id: '1', label_seq_id: 115 }
+                        ]
+                    }]
+                };
+
                 viewer.clear()
                     .then(function() {
-                        return viewer.loadPdbIds([{
-                            pdbId: '5CBG',
-                            props: {
-                                label: '5CBG',
-                                kind: 'motif',
-                                // assemblyId: '2', // library should be able to infer assemblyId of the query
-                                targets: [
-                                    { label_asym_id: 'C', label_seq_id: 30, struct_oper_id: '3' },
-                                    { label_asym_id: 'C', label_seq_id: 32, struct_oper_id: '3' },
-                                    { label_asym_id: 'C', label_seq_id: 34, struct_oper_id: '3' }
-                                ],
-                                // color: 13203230
-                            }
-                        }, {
-                            pdbId: '2W02',
-                            props: {
-                                label: '2W02 #1',
-                                kind: 'motif',
-                                assemblyId: '2',
-                                targets: [
-                                    { label_asym_id: 'B', label_seq_id: 519 },
-                                    { label_asym_id: 'B', label_seq_id: 517 },
-                                    { label_asym_id: 'B', label_seq_id: 515 }
-                                ],
-                                // color: 4947916
-                            },
-                            matrix: [
-                                0.7953831708253857, -0.6048923987758781, 0.03835097744411625, 0,
-                                -0.23732979915044658, -0.2525976533016715, 0.9380133218572628, 0,
-                                -0.5577097614377604, -0.7551818399893184, -0.344470913935246, 0,
-                                154.77888998938096, 207.0215940587305, 25.17873980937774, 1
-                            ]
-                        }]);
-                    })
-                    .then(function() {
-                        viewer.resetCamera(0)
+                        return viewer.alignMotifs(payload);
                     });
             }
 

+ 23 - 0
src/viewer/index.ts

@@ -37,6 +37,7 @@ import { DefaultPluginUISpec, PluginUISpec } from 'molstar/lib/mol-plugin-ui/spe
 import { PluginUIContext } from 'molstar/lib/mol-plugin-ui/context';
 import { ANVILMembraneOrientation, MembraneOrientationPreset } from 'molstar/lib/extensions/anvil/behavior';
 import { MembraneOrientationRepresentationProvider } from 'molstar/lib/extensions/anvil/representation';
+import { MotifAlignmentRequest, alignMotifs } from './helpers/superpose/pecos-integration';
 
 /** package version, filled in at bundle build time */
 declare const __RCSB_MOLSTAR_VERSION__: string;
@@ -244,6 +245,28 @@ export class Viewer {
         this.resetCamera(0);
     }
 
+    async alignMotifs(request: MotifAlignmentRequest) {
+        const { query, hits } = request;
+
+        await this.loadPdbId(query.entry_id,
+            {
+                kind: 'motif',
+                label: query.entry_id,
+                targets: query.residue_ids
+            });
+
+        for (const hit of hits) {
+            const { rmsd, matrix } = await alignMotifs(query, hit);
+            await this.loadPdbId(hit.entry_id, {
+                kind: 'motif',
+                assemblyId: hit.assembly_id,
+                label: `${hit.entry_id} #${hit.id}: ${rmsd.toFixed(2)} RMSD`,
+                targets: hit.residue_ids
+            }, matrix);
+            this.resetCamera(0);
+        }
+    }
+
     loadStructureFromUrl(url: string, format: BuiltInTrajectoryFormat, isBinary: boolean, props?: PresetProps, matrix?: Mat4) {
         return this.customState.modelLoader.load({ fileOrUrl: url, format, isBinary }, props, matrix);
     }