Explorar el Código

Merge pull request #4 from rcsb/dev-sb-motifvis

Visualize Motif Alignments & Refactoring
Sebastian Bittrich hace 3 años
padre
commit
eec9b00ad3

+ 7 - 0
CHANGELOG.md

@@ -2,6 +2,13 @@
 
 [Semantic Versioning](https://semver.org/)
 
+## [1.7.0] - 2021-06-24
+### Added
+- Visualize (an arbitrary number of) structural motifs
+
+### Bug fixes
+- Fix order of operators in strucmotif queries
+
 ## [1.6.9] - 2021-06-23
 ### Added
 - Mol* 2.0.7 & some internal cleanup

+ 9 - 9
package-lock.json

@@ -1,12 +1,12 @@
 {
     "name": "@rcsb/rcsb-molstar",
-    "version": "1.6.9",
+    "version": "1.7.0-dev.1",
     "lockfileVersion": 2,
     "requires": true,
     "packages": {
         "": {
             "name": "@rcsb/rcsb-molstar",
-            "version": "1.6.9",
+            "version": "1.7.0-dev.1",
             "license": "MIT",
             "devDependencies": {
                 "@types/react": "^17.0.2",
@@ -21,7 +21,7 @@
                 "file-loader": "^6.2.0",
                 "fs-extra": "^9.0.1",
                 "mini-css-extract-plugin": "^1.3.2",
-                "molstar": "^2.0.7",
+                "molstar": "^2.0.6",
                 "node-sass": "^5.0.0",
                 "raw-loader": "^4.0.2",
                 "react": "^17.0.1",
@@ -5527,9 +5527,9 @@
             }
         },
         "node_modules/molstar": {
-            "version": "2.0.7",
-            "resolved": "https://registry.npmjs.org/molstar/-/molstar-2.0.7.tgz",
-            "integrity": "sha512-MLE/lmmLS9DXV68IG1AXCtLUgNiS2KhdA4ux/5nZmXdMcNebS7Z8RjArJ2Z+Ok9l1hSX/LpGt/9mRtuuySLhpQ==",
+            "version": "2.0.6",
+            "resolved": "https://registry.npmjs.org/molstar/-/molstar-2.0.6.tgz",
+            "integrity": "sha512-EfX5DEcbHgticllgX/Fr8O26C4gHUthk8F2zXBQiQe2fmg/0HDjqXBJYgwAra3cxETrpQxIDarudb440tAnR7w==",
             "dev": true,
             "dependencies": {
                 "@types/argparse": "^1.0.38",
@@ -14387,9 +14387,9 @@
             }
         },
         "molstar": {
-            "version": "2.0.7",
-            "resolved": "https://registry.npmjs.org/molstar/-/molstar-2.0.7.tgz",
-            "integrity": "sha512-MLE/lmmLS9DXV68IG1AXCtLUgNiS2KhdA4ux/5nZmXdMcNebS7Z8RjArJ2Z+Ok9l1hSX/LpGt/9mRtuuySLhpQ==",
+            "version": "2.0.6",
+            "resolved": "https://registry.npmjs.org/molstar/-/molstar-2.0.6.tgz",
+            "integrity": "sha512-EfX5DEcbHgticllgX/Fr8O26C4gHUthk8F2zXBQiQe2fmg/0HDjqXBJYgwAra3cxETrpQxIDarudb440tAnR7w==",
             "dev": true,
             "requires": {
                 "@types/argparse": "^1.0.38",

+ 53 - 216
src/viewer/helpers/preset.ts

@@ -5,8 +5,6 @@
  */
 
 import { PluginContext } from 'molstar/lib/mol-plugin/context';
-import { MolScriptBuilder as MS } from 'molstar/lib/mol-script/language/builder';
-import { Expression } from 'molstar/lib/mol-script/language/expression';
 import { ParamDefinition as PD } from 'molstar/lib/mol-util/param-definition';
 import { TrajectoryHierarchyPresetProvider } from 'molstar/lib/mol-plugin-state/builder/structure/hierarchy-preset';
 import { ValidationReportGeometryQualityPreset } from 'molstar/lib/extensions/rcsb/validation-report/behavior';
@@ -14,26 +12,17 @@ import { AssemblySymmetryPreset } from 'molstar/lib/extensions/rcsb/assembly-sym
 import { PluginStateObject } from 'molstar/lib/mol-plugin-state/objects';
 import { RootStructureDefinition } from 'molstar/lib/mol-plugin-state/helpers/root-structure';
 import { StructureRepresentationPresetProvider } from 'molstar/lib/mol-plugin-state/builder/structure/representation-preset';
-import {
-    Structure,
-    StructureSelection,
-    QueryContext,
-    StructureElement
-} from 'molstar/lib/mol-model/structure';
-import { compile } from 'molstar/lib/mol-script/runtime/query/compiler';
+import { StructureElement } from 'molstar/lib/mol-model/structure';
 import { ViewerState } from '../types';
 import {
     StateSelection,
     StateObjectSelector,
     StateObject,
-    StateTransformer,
-    StateObjectRef
+    StateTransformer
 } from 'molstar/lib/mol-state';
 import { Mat4 } from 'molstar/lib/mol-math/linear-algebra';
 import { CustomStructureProperties } from 'molstar/lib/mol-plugin-state/transforms/model';
-import { FlexibleStructureFromModel as FlexibleStructureFromModel } from './superpose/flexible-structure';
-import { StructureRepresentationRegistry } from 'molstar/lib/mol-repr/structure/registry';
-import { StructureSelectionQueries as Q } from 'molstar/lib/mol-plugin-state/helpers/structure-selection-query';
+import { FlexibleStructureFromModel } from './superpose/flexible-structure';
 import { PluginCommands } from 'molstar/lib/mol-plugin/commands';
 import { InteractivityManager } from 'molstar/lib/mol-plugin-state/manager/interactivity';
 import { MembraneOrientationPreset } from 'molstar/lib/extensions/anvil/behavior';
@@ -43,56 +32,16 @@ import {
     InitVolumeStreaming,
     VolumeStreamingVisual
 } from 'molstar/lib/mol-plugin/behavior/dynamic/volume-streaming/transformers';
-
-type Target = {
-    readonly auth_seq_id?: number
-    readonly label_seq_id?: number
-    readonly label_comp_id?: string
-    readonly label_asym_id?: string
-}
-
-function targetToExpression(target: Target): Expression {
-    const residueTests: Expression[] = [];
-    const tests = Object.create(null);
-
-    if (target.auth_seq_id) {
-        residueTests.push(MS.core.rel.eq([target.auth_seq_id, MS.ammp('auth_seq_id')]));
-    } else if (target.label_seq_id) {
-        residueTests.push(MS.core.rel.eq([target.label_seq_id, MS.ammp('label_seq_id')]));
-    }
-    if (target.label_comp_id) {
-        residueTests.push(MS.core.rel.eq([target.label_comp_id, MS.ammp('label_comp_id')]));
-    }
-    if (residueTests.length === 1) {
-        tests['residue-test'] = residueTests[0];
-    } else if (residueTests.length > 1) {
-        tests['residue-test'] = MS.core.logic.and(residueTests);
-    }
-
-    if (target.label_asym_id) {
-        tests['chain-test'] = MS.core.rel.eq([target.label_asym_id, MS.ammp('label_asym_id')]);
-    }
-
-    if (Object.keys(tests).length > 0) {
-        return MS.struct.modifier.union([
-            MS.struct.generator.atomGroups(tests)
-        ]);
-    } else {
-        return MS.struct.generator.empty;
-    }
-}
-
-function targetToLoci(target: Target, structure: Structure): StructureElement.Loci {
-    const expression = targetToExpression(target);
-    const query = compile<StructureSelection>(expression);
-    const selection = query(new QueryContext(structure));
-    return StructureSelection.toLociWithSourceUnits(selection);
-}
-
-type Range = {
-    label_asym_id: string
-    label_seq_id?: { beg: number, end?: number }
-}
+import {
+    createSelectionExpressions,
+    normalizeTargets,
+    Range,
+    SelectionExpression,
+    Target,
+    targetToLoci,
+    toRange
+} from './selection';
+import { RcsbSuperpositionRepresentationPreset } from './superpose/preset';
 
 type BaseProps = {
     assemblyId?: string
@@ -152,9 +101,17 @@ type FeatureDensityProps = {
     hiddenChannels?: string[]
 } & BaseProps
 
-export type PresetProps = ValidationProps | StandardProps | SymmetryProps | FeatureProps | DensityProps | PropsetProps | MembraneProps | FeatureDensityProps | EmptyProps;
+export type MotifProps = {
+    kind: 'motif',
+    label?: string,
+    targets: Target[],
+    color?: number
+} & BaseProps
+
+export type PresetProps = ValidationProps | StandardProps | SymmetryProps | FeatureProps | DensityProps | PropsetProps |
+MembraneProps | FeatureDensityProps | MotifProps | EmptyProps;
 
-const RcsbParams = (a: PluginStateObject.Molecule.Trajectory | undefined, plugin: PluginContext) => ({
+const RcsbParams = () => ({
     preset: PD.Value<PresetProps>({ kind: 'standard', assemblyId: '' }, { isHidden: true })
 });
 
@@ -162,70 +119,10 @@ type StructureObject = StateObjectSelector<PluginStateObject.Molecule.Structure,
 
 const CommonParams = StructureRepresentationPresetProvider.CommonParams;
 
-const reprBuilder = StructureRepresentationPresetProvider.reprBuilder;
-const updateFocusRepr = StructureRepresentationPresetProvider.updateFocusRepr;
-
-type SelectionExpression = {
-    tag: string
-    type: StructureRepresentationRegistry.BuiltIn
-    label: string
-    expression: Expression
-};
-
-export const RcsbSuperpositionRepresentationPreset = StructureRepresentationPresetProvider({
-    id: 'preset-superposition-representation-rcsb',
-    display: {
-        group: 'Superposition',
-        name: 'Alignment',
-        description: 'Show representations based on the structural alignment data.'
-    },
-    params: () => ({
-        ...CommonParams,
-        selectionExpressions: PD.Value<SelectionExpression[]>([])
-    }),
-    async apply(ref, params, plugin) {
-
-        const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
-        if (!structureCell) return Object.create(null);
-
-        const structure = structureCell.obj!.data;
-        const cartoonProps = {sizeFactor: structure.isCoarseGrained ? 0.8 : 0.2};
-
-        let components = Object.create(null);
-        let representations = Object.create(null);
-        for (const expr of params.selectionExpressions) {
-
-            const comp = await plugin.builders.structure.tryCreateComponentFromExpression(structureCell, expr.expression, expr.label, { label: expr.label });
-            Object.assign(components, {[expr.label]: comp});
-
-            const { update, builder, typeParams, color } = reprBuilder(plugin, params);
-
-            let typeProps = {...typeParams};
-            if (expr.type === 'cartoon') {
-                Object.assign(typeProps, {...cartoonProps});
-            }
-
-            Object.assign(representations, {
-                [expr.label]: builder.buildRepresentation(update, comp, {type: expr.type,
-                    typeParams: typeProps, color: color as any}, { tag: expr.tag }),
-            });
-
-            await update.commit({ revertOnError: false });
-
-        }
-        // needed to apply same coloring scheme to focus representation
-        await updateFocusRepr(plugin, structure, params.theme?.focus?.name, params.theme?.focus?.params);
-
-        return representations;
-    }
-});
-
 export const RcsbPreset = TrajectoryHierarchyPresetProvider({
     id: 'preset-trajectory-rcsb',
     display: { name: 'RCSB' },
-    isApplicable: o => {
-        return true;
-    },
+    isApplicable: () => true,
     params: RcsbParams,
     async apply(trajectory, params, plugin) {
         const builder = plugin.builders.structure;
@@ -253,7 +150,11 @@ export const RcsbPreset = TrajectoryHierarchyPresetProvider({
         if (!allowsFlexTransform) {
             structure = await builder.createStructure(modelProperties || model, structureParams);
             structureProperties = await builder.insertStructureProperties(structure);
-            unitcell = await builder.tryCreateUnitcell(modelProperties, undefined, { isHidden: true });
+
+            // hide unit cell when dealing with motifs
+            if (p.kind !== 'motif') {
+                unitcell = await builder.tryCreateUnitcell(modelProperties, undefined, { isHidden: true });
+            }
         }
 
         let representation: StructureRepresentationPresetProvider.Result | undefined = undefined;
@@ -292,23 +193,39 @@ export const RcsbPreset = TrajectoryHierarchyPresetProvider({
             let selectionExpressions: SelectionExpression[] = [];
             if (p.selection) {
                 for (const range of p.selection) {
-                    selectionExpressions = selectionExpressions.concat(createSelectionExpression(entryId, range));
+                    selectionExpressions = selectionExpressions.concat(createSelectionExpressions(entryId, range));
                 }
             } else {
-                selectionExpressions = selectionExpressions.concat(createSelectionExpression(entryId));
+                selectionExpressions = selectionExpressions.concat(createSelectionExpressions(entryId));
             }
 
             const params = {
                 ignoreHydrogens: CommonParams.ignoreHydrogens.defaultValue,
                 quality: CommonParams.quality.defaultValue,
-                theme: { globalName: 'superpose' as any, focus: { name: 'superpose' } },
+                theme: { globalName: 'superpose', focus: { name: 'superpose' } },
                 selectionExpressions: selectionExpressions
             };
-            representation = await RcsbSuperpositionRepresentationPreset.apply(structure, params, plugin);
+            representation = await plugin.builders.structure.representation.applyPreset<any>(structureProperties!, RcsbSuperpositionRepresentationPreset, params);
+        } else if (p.kind === 'motif' && structure?.obj) {
+            // let's force ASM_1 for motifs (as we use this contract in the rest of the stack)
+            // TODO should ASM_1 be the default, seems like we'd run into problems when selecting ligands that are e.g. ambiguous with asym_id & seq_id alone?
+            const targets = normalizeTargets(p.targets, structure!.obj.data);
+            let selectionExpressions = createSelectionExpressions(p.label || model.data!.entryId, targets);
+            const globalExpressions = createSelectionExpressions(p.label || model.data!.entryId); // global reps, to be hidden
+            selectionExpressions = selectionExpressions.concat(globalExpressions.map(e => { return { ...e, isHidden: true }; }));
+
+            if (p.color) {
+                selectionExpressions = selectionExpressions.map(e => { return { ...e, color: p.color }; });
+            }
 
+            const params = {
+                ignoreHydrogens: true,
+                quality: CommonParams.quality.defaultValue,
+                selectionExpressions: selectionExpressions
+            };
+            representation = await plugin.builders.structure.representation.applyPreset<any>(structureProperties!, RcsbSuperpositionRepresentationPreset, params);
         } else if (p.kind === 'validation') {
             representation = await plugin.builders.structure.representation.applyPreset(structureProperties!, ValidationReportGeometryQualityPreset);
-
         } else if (p.kind === 'symmetry') {
             representation = await plugin.builders.structure.representation.applyPreset<any>(structureProperties!, AssemblySymmetryPreset, { symmetryIndex: p.symmetryIndex });
 
@@ -325,17 +242,17 @@ export const RcsbPreset = TrajectoryHierarchyPresetProvider({
         }
 
         if ((p.kind === 'feature' || p.kind === 'feature-density') && structure?.obj) {
-            let loci = targetToLoci(p.target, structure.obj.data);
+            let loci = targetToLoci(p.target, structure!.obj.data);
             // if target is only defined by chain: then don't force first residue
             const chainMode = p.target.label_asym_id && !p.target.auth_seq_id && !p.target.label_seq_id && !p.target.label_comp_id;
             // HELP-16678: check for rare case where ligand is not present in requested assembly
             if (loci.elements.length === 0 && !!p.assemblyId) {
-                // switch to Model (a.k.a. show coordinate independent of assembly )
+                // switch to Model (a.k.a. show coordinates independent of assembly)
                 const { selection } = plugin.managers.structure.hierarchy;
                 const s = selection.structures[0];
                 await plugin.managers.structure.hierarchy.updateStructure(s, { ...params, preset: { ...params.preset, assemblyId: void 0 } });
                 // update loci
-                loci = targetToLoci(p.target, structure.obj.data);
+                loci = targetToLoci(p.target, structure!.obj.data);
             }
             const target = chainMode ? loci : StructureElement.Loci.firstResidue(loci);
 
@@ -409,83 +326,3 @@ async function initVolumeStreaming(plugin: PluginContext, structure: StructureOb
         volume: false
     });
 }
-
-export function createSelectionExpression(entryId: string, range?: Range): SelectionExpression[] {
-    if (range) {
-        const residues: number[] = (range.label_seq_id) ? toRange(range.label_seq_id.beg, range.label_seq_id.end) : [];
-        const test = selectionTest(range.label_asym_id, residues);
-        const label = labelFromProps(entryId, range);
-        return [{
-            expression: MS.struct.generator.atomGroups(test),
-            label: `${label}`,
-            type: 'cartoon',
-            tag: 'polymer'
-        }];
-    } else {
-        return [
-            {
-                expression: Q.polymer.expression,
-                label: `${entryId} - Polymers`,
-                type: 'cartoon',
-                tag: 'polymer'
-            },
-            {
-                expression: Q.ligand.expression,
-                label: `${entryId} - Ligands`,
-                type: 'ball-and-stick',
-                tag: 'ligand'
-            },
-            {
-                expression: Q.ion.expression,
-                label: `${entryId} - Ions`,
-                type: 'ball-and-stick',
-                tag: 'ion'
-            },
-            {
-                expression: Q.branched.expression,
-                label: `${entryId} - Carbohydrates`,
-                type: 'carbohydrate',
-                tag: 'branched-snfg-3d'
-            },
-            {
-                expression: Q.lipid.expression,
-                label: `${entryId} - Lipids`,
-                type: 'ball-and-stick',
-                tag: 'lipid'
-            },
-            {
-                expression: Q.water.expression,
-                label: `${entryId} - Waters`,
-                type: 'ball-and-stick',
-                tag: 'water'
-            }
-        ];
-    }
-}
-
-export const selectionTest = (asymId: string, residues: number[]) => {
-    if (residues.length > 0) {
-        return {
-            'chain-test': MS.core.rel.eq([MS.ammp('label_asym_id'), asymId]),
-            'residue-test': MS.core.set.has([MS.set(...residues), MS.ammp('label_seq_id')])
-        };
-    } else {
-        return { 'chain-test': MS.core.rel.eq([MS.ammp('label_asym_id'), asymId]) };
-    }
-};
-
-export const toRange = (start: number, end?: number) => {
-    if (!end) return [start];
-    const b = start < end ? start : end;
-    const e = start < end ? end : start;
-    return [...Array(e - b + 1)].map((_, i) => b + i);
-};
-
-const labelFromProps = (entryId: string, range: Range) => {
-
-    const residues: number[] = (range.label_seq_id) ? toRange(range.label_seq_id.beg, range.label_seq_id.end) : [];
-    const label = entryId + (range.label_asym_id ? `.${range.label_asym_id}` : '') +
-        (residues && residues.length > 0 ? `:${residues[0]}` : '') +
-        (residues && residues.length > 1 ? `-${residues[residues.length - 1]}` : '');
-    return label;
-};

+ 219 - 0
src/viewer/helpers/selection.ts

@@ -0,0 +1,219 @@
+import { MolScriptBuilder as MS } from 'molstar/lib/mol-script/language/builder';
+import { StructureSelectionQueries as Q } from 'molstar/lib/mol-plugin-state/helpers/structure-selection-query';
+import { StructureRepresentationRegistry } from 'molstar/lib/mol-repr/structure/registry';
+import { Expression } from 'molstar/lib/mol-script/language/expression';
+import { QueryContext, Structure, StructureElement, StructureSelection } from 'molstar/lib/mol-model/structure';
+import { compile } from 'molstar/lib/mol-script/runtime/query/compiler';
+
+export type Target = {
+    readonly auth_seq_id?: number
+    readonly label_seq_id?: number
+    readonly label_comp_id?: string
+    readonly label_asym_id?: string
+    /**
+     * Mol*-internal representation, like 'ASM_2'. Enumerated in the order of appearance in the source file. Specify the
+     * assemblyId when using this selector.
+     */
+    readonly operatorName?: string
+    /**
+     * Strucmotif-/BioJava-specific representation, like 'Px42'. This is a single 'pdbx_struct_oper_list.id' value or a
+     * combination thereof. Specify the assemblyId when using this selector. Order matters, use order as specified in
+     * the source CIF file.
+     */
+    readonly structOperExpression?: string
+}
+
+export type Range = {
+    label_asym_id: string
+    label_seq_id?: { beg: number, end?: number }
+}
+
+export const toRange = (start: number, end?: number) => {
+    if (!end) return [start];
+    const b = start < end ? start : end;
+    const e = start < end ? end : start;
+    return [...Array(e - b + 1)].map((_, i) => b + i);
+};
+
+export type SelectionExpression = {
+    tag: string
+    type: StructureRepresentationRegistry.BuiltIn
+    label: string
+    expression: Expression
+    isHidden?: boolean,
+    color?: number
+};
+
+/**
+ * This serves as adapter between the strucmotif-/BioJava-approach to identify transformed chains and the Mol* way.
+ * Looks for 'structOperExpression', converts it to an 'operatorName', and removes the original value. This will
+ * override pre-existing 'operatorName' values.
+ * @param targets collection to process
+ * @param structure parent structure
+ * @param operatorName optional value to which missing operators are set, will default to 'ASM_1' if not specified
+ */
+export function normalizeTargets(targets: Target[], structure: Structure, operatorName: string = 'ASM_1'): Target[] {
+    return targets.map(t => {
+        if (t.structOperExpression) {
+            const { structOperExpression, ...others } = t;
+            const oper = toOperatorName(structure, structOperExpression);
+            return { ...others, operatorName: oper };
+        }
+        return t.operatorName ? t : { ...t, operatorName };
+    });
+}
+
+function toOperatorName(structure: Structure, expression: string): string {
+    // Mol*-internal representation is flipped ('5xX0' insteadof 'X0x5')
+    expression = expression.indexOf('x') === -1 ? expression : expression.split('x').reverse().join('x');
+    for (const unit of structure.units) {
+        const assembly = unit.conformation.operator.assembly;
+        if (!assembly) continue;
+
+        if (expression === assembly.operList.join('x')) return `ASM_${assembly.operId}`;
+    }
+    // TODO better error handling?
+    throw Error(`Unable to find expression '${expression}'`);
+}
+
+/**
+ * Convert a selection to an array of selection expressions.
+ * @param labelBase the base label that will appear in the UI (e.g., the entry ID)
+ * @param selection a selection by Range or a set of Targets
+ */
+export function createSelectionExpressions(labelBase: string, selection?: Range | Target[]): SelectionExpression[] {
+    if (selection) {
+        if ('label_asym_id' in selection && 'label_seq_id' in selection) {
+            const range = selection as Range;
+            const residues: number[] = (range.label_seq_id) ? toRange(range.label_seq_id.beg, range.label_seq_id.end) : [];
+            const test = rangeToTest(range.label_asym_id, residues);
+            const label = labelFromProps(labelBase, range);
+            return [{
+                expression: MS.struct.generator.atomGroups(test),
+                label: `${label}`,
+                type: 'cartoon',
+                tag: 'polymer'
+            }];
+        } else if (Array.isArray(selection)) {
+            const expression = targetsToExpression(selection);
+            return [{
+                expression: expression,
+                label: `${labelBase}`,
+                type: 'ball-and-stick',
+                tag: 'polymer'
+            }];
+        } else {
+            throw Error('Unable to handle selection: ' + selection);
+        }
+    } else {
+        return [
+            {
+                expression: Q.polymer.expression,
+                label: `${labelBase} - Polymers`,
+                type: 'cartoon',
+                tag: 'polymer'
+            },
+            {
+                expression: Q.ligand.expression,
+                label: `${labelBase} - Ligands`,
+                type: 'ball-and-stick',
+                tag: 'ligand'
+            },
+            {
+                expression: Q.ion.expression,
+                label: `${labelBase} - Ions`,
+                type: 'ball-and-stick',
+                tag: 'ion'
+            },
+            {
+                expression: Q.branched.expression,
+                label: `${labelBase} - Carbohydrates`,
+                type: 'carbohydrate',
+                tag: 'branched-snfg-3d'
+            },
+            {
+                expression: Q.lipid.expression,
+                label: `${labelBase} - Lipids`,
+                type: 'ball-and-stick',
+                tag: 'lipid'
+            },
+            {
+                expression: Q.water.expression,
+                label: `${labelBase} - Waters`,
+                type: 'ball-and-stick',
+                tag: 'water'
+            }
+        ];
+    }
+}
+
+const labelFromProps = (entryId: string, range: Range) => {
+    const residues: number[] = (range.label_seq_id) ? toRange(range.label_seq_id.beg, range.label_seq_id.end) : [];
+    return entryId + (range.label_asym_id ? `.${range.label_asym_id}` : '') +
+        (residues && residues.length > 0 ? `:${residues[0]}` : '') +
+        (residues && residues.length > 1 ? `-${residues[residues.length - 1]}` : '');
+};
+
+export function rangeToTest(asymId: string, residues: number[]) {
+    if (residues.length > 0) {
+        return {
+            'chain-test': MS.core.rel.eq([MS.ammp('label_asym_id'), asymId]),
+            'residue-test': MS.core.set.has([MS.set(...residues), MS.ammp('label_seq_id')])
+        };
+    } else {
+        return { 'chain-test': MS.core.rel.eq([MS.ammp('label_asym_id'), asymId]) };
+    }
+}
+
+export function targetToLoci(target: Target, structure: Structure): StructureElement.Loci {
+    const expression = targetToExpression(target);
+    const query = compile<StructureSelection>(expression);
+    const selection = query(new QueryContext(structure));
+    return StructureSelection.toLociWithSourceUnits(selection);
+}
+
+function targetsToExpression(targets: Target[]): Expression {
+    const expressions = targets.map(t => targetToExpression(t));
+    return MS.struct.combinator.merge(expressions);
+}
+
+function targetToExpression(target: Target): Expression {
+    const residueTests: Expression[] = [];
+    const chainTests: Expression[] = [];
+    const tests: { 'residue-test': Expression, 'chain-test': Expression } = Object.create(null);
+
+    if (target.auth_seq_id) {
+        residueTests.push(MS.core.rel.eq([target.auth_seq_id, MS.ammp('auth_seq_id')]));
+    } else if (target.label_seq_id) {
+        residueTests.push(MS.core.rel.eq([target.label_seq_id, MS.ammp('label_seq_id')]));
+    }
+    if (target.label_comp_id) {
+        residueTests.push(MS.core.rel.eq([target.label_comp_id, MS.ammp('label_comp_id')]));
+    }
+    if (residueTests.length === 1) {
+        tests['residue-test'] = residueTests[0];
+    } else if (residueTests.length > 1) {
+        tests['residue-test'] = MS.core.logic.and(residueTests);
+    }
+
+    if (target.label_asym_id) {
+        chainTests.push(MS.core.rel.eq([target.label_asym_id, MS.ammp('label_asym_id')]));
+    }
+    if (target.operatorName) {
+        chainTests.push(MS.core.rel.eq([target.operatorName, MS.acp('operatorName')]));
+    }
+
+    if (chainTests.length === 1) {
+        tests['chain-test'] = chainTests[0];
+    } else if (chainTests.length > 1) {
+        tests['chain-test'] = MS.core.logic.and(chainTests);
+    }
+
+    if (Object.keys(tests).length > 0) {
+        return MS.struct.modifier.union([
+            MS.struct.generator.atomGroups(tests)
+        ]);
+    } else {
+        return MS.struct.generator.empty;
+    }
+}

+ 3 - 2
src/viewer/helpers/superpose/flexible-structure.ts

@@ -3,10 +3,11 @@ import { RootStructureDefinition } from 'molstar/lib/mol-plugin-state/helpers/ro
 import { PluginContext } from 'molstar/lib/mol-plugin/context';
 import { Task } from 'molstar/lib/mol-task';
 import { ParamDefinition as PD } from 'molstar/lib/mol-util/param-definition';
-import { PropsetProps, selectionTest, toRange } from '../preset';
+import { PropsetProps } from '../preset';
 import { StructureQueryHelper } from 'molstar/lib/mol-plugin-state/helpers/structure-query';
 import { MolScriptBuilder as MS } from 'molstar/lib/mol-script/language/builder';
 import { StructureSelection, Structure } from 'molstar/lib/mol-model/structure';
+import { rangeToTest, toRange } from '../selection';
 
 export { FlexibleStructureFromModel as FlexibleStructureFromModel };
 type FlexibleStructureFromModel = typeof FlexibleStructureFromModel
@@ -36,7 +37,7 @@ const FlexibleStructureFromModel = PluginStateTransform.BuiltIn({
                     selectBlocks.push([]);
                 }
                 const residues: number[] = (p.label_seq_id) ? toRange(p.label_seq_id.beg, p.label_seq_id.end) : [];
-                const test = selectionTest(p.label_asym_id, residues);
+                const test = rangeToTest(p.label_asym_id, residues);
                 const expression = MS.struct.generator.atomGroups(test);
                 const { selection: sele } = StructureQueryHelper.createAndRun(base.data, expression);
                 const s = StructureSelection.unionStructure(sele);

+ 79 - 0
src/viewer/helpers/superpose/preset.ts

@@ -0,0 +1,79 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Yana Rose
+ */
+
+import { StructureRepresentationPresetProvider } from 'molstar/lib/mol-plugin-state/builder/structure/representation-preset';
+import { ParamDefinition as PD } from 'molstar/lib/mol-util/param-definition';
+import { SelectionExpression } from '../selection';
+import { StateObjectRef } from 'molstar/lib/mol-state';
+import reprBuilder = StructureRepresentationPresetProvider.reprBuilder;
+import updateFocusRepr = StructureRepresentationPresetProvider.updateFocusRepr;
+import { StateTransform } from 'molstar/lib/mol-state/transform';
+
+export const RcsbSuperpositionRepresentationPreset = StructureRepresentationPresetProvider({
+    id: 'preset-superposition-representation-rcsb',
+    display: {
+        group: 'Superposition',
+        name: 'Alignment',
+        description: 'Show representations based on the structural alignment data.'
+    },
+    params: () => ({
+        ...StructureRepresentationPresetProvider.CommonParams,
+        selectionExpressions: PD.Value<SelectionExpression[]>([])
+    }),
+    async apply(ref, params, plugin) {
+        const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
+        if (!structureCell) return {};
+
+        const structure = structureCell.obj!.data;
+        const cartoonProps = { sizeFactor: structure.isCoarseGrained ? 0.8 : 0.2 };
+
+        let components = Object.create(null);
+        let representations = Object.create(null);
+
+        for (const expr of params.selectionExpressions) {
+            const comp = await plugin.builders.structure.tryCreateComponentFromExpression(structureCell, expr.expression, expr.label, { label: expr.label });
+            Object.assign(components, { [expr.label]: comp });
+
+            const { update, builder, typeParams, color } = reprBuilder(plugin, params);
+
+            const typeProps = { ...typeParams };
+            if (expr.type === 'cartoon') {
+                Object.assign(typeProps, { ...cartoonProps });
+            }
+
+            const reprProps = {
+                type: expr.type,
+                typeParams: typeProps,
+                color: color as any
+            };
+            if (expr.color) {
+                Object.assign(reprProps, {
+                    color: 'uniform',
+                    colorParams: { value: expr.color }
+                });
+            }
+
+            Object.assign(representations, {
+                [expr.label]: builder.buildRepresentation(update, comp, reprProps, {
+                    tag: expr.tag,
+                    // this only hides the visuals but the state UI will still indicate them as visible
+                    initialState: { isHidden: expr.isHidden || false }
+                })
+            });
+            // make sure UI state is consistent
+            if (comp?.cell?.state && expr.isHidden) {
+                StateTransform.assignState(comp?.cell?.state, { isHidden: true });
+            }
+
+            await update.commit({ revertOnError: false });
+        }
+
+        // needed to apply same coloring scheme to focus representation
+        await updateFocusRepr(plugin, structure, params.theme?.focus?.name, params.theme?.focus?.params);
+
+        return representations;
+    }
+});

+ 162 - 5
src/viewer/index.html

@@ -88,8 +88,12 @@
 
             &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
 
-            Ligand Focus on wrong assembly
-            <button style="padding: 3px" onclick="ligandAssembly()">5RLA</button>
+            Superpose Motifs
+            <button style="padding: 3px;" onclick="motifs()">4CHA | 6YIW</button>
+            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
+
+            Superpose Propset
+            <button style="padding: 3px" onclick="propset()">4HHB | 1OJ6</button>
         </div>
         <script>
 
@@ -321,6 +325,57 @@
                         // radius: 0,
                         // hiddenChannels: ['fo-fc(+ve)', 'fo-fc(-ve)']
                     }
+                },
+                {
+                    id: '5RL9',
+                    info: 'ligand focus on wrong assembly: Crystal Structure of SARS-CoV-2 helicase in complex with Z1703168683',
+                    props: {
+                        kind: 'feature',
+                        assemblyId: '1',
+                        // UR7 is not present in assembly 1
+                        target: {
+                            label_comp_id: 'UR7'
+                        }
+                    }
+                },
+                {
+                    id: '5VL5',
+                    info: 'motif selection with operator: Coordination Chemistry within a Protein Host: Regulation of the Secondary Coordination Sphere',
+                    props: {
+                        kind: 'motif',
+                        assemblyId: '1',
+                        targets: [
+                            { label_asym_id: 'A', label_seq_id: 61 },
+                            { label_asym_id: 'A', label_seq_id: 69 },
+                            { label_asym_id: 'A', label_seq_id: 87, operatorName: 'ASM_4' }
+                        ],
+                    }
+                },
+                {
+                    id: '5VL5',
+                    info: 'motif selection with strucmotif-expression: Coordination Chemistry within a Protein Host: Regulation of the Secondary Coordination Sphere',
+                    props: {
+                        kind: 'motif',
+                        assemblyId: '1',
+                        targets: [
+                            { label_asym_id: 'A', label_seq_id: 61 },
+                            { label_asym_id: 'A', label_seq_id: 69 },
+                            { label_asym_id: 'A', label_seq_id: 87, structOperExpression: '4' }
+                        ],
+                    }
+                },
+                {
+                    id: '2BFU',
+                    info: 'motif selection with strucmotif-expression: X-ray structure of CPMV top component',
+                    props: {
+                        kind: 'motif',
+                        assemblyId: '6',
+                        targets: [
+                            { label_asym_id: 'A', label_seq_id: 46, structOperExpression: 'X0x5' },
+                            { label_asym_id: 'A', label_seq_id: 49, structOperExpression: 'X0x5' },
+                            { label_asym_id: 'A', label_seq_id: 145, structOperExpression: 'X0x5' }
+                        ],
+                    }
                 }
             ];
 
@@ -352,11 +407,113 @@
                     });
             }
 
-            function ligandAssembly() {
+            function motifs() {
                 viewer.clear()
                     .then(function() {
-                        // UR7 is not present in assembly 1
-                        return viewer.loadPdbId('5RL9', { kind: 'feature', assemblyId: '1', target: { label_comp_id: 'UR7' } });
+                        return viewer.loadPdbIds([{
+                            pdbId: '4cha',
+                            props: {
+                                label: '4CHA',
+                                kind: 'motif',
+                                assemblyId: '1',
+                                targets: [
+                                    { label_asym_id: 'B', label_seq_id: 42 },
+                                    { label_asym_id: 'B', label_seq_id: 87 },
+                                    { label_asym_id: 'C', label_seq_id: 47 }
+                                ],
+                                // color: 13203230
+                            }
+                        }, {
+                            pdbId: '6yiw',
+                            props: {
+                                label: '6YIW #1',
+                                kind: 'motif',
+                                assemblyId: '1',
+                                targets: [
+                                    { label_asym_id: 'A', label_seq_id: 40 },
+                                    { label_asym_id: 'A', label_seq_id: 84 },
+                                    { label_asym_id: 'A', label_seq_id: 177 }
+                                ],
+                                // color: 4947916
+                            },
+                            matrix: [
+                                0.1651637134205112, 0.7020365618749254, 0.6927233311791812, 0,
+                                0.39076998819946046, 0.5983062863806071, -0.6995201240851049, 0,
+                                -0.9055494266420474, 0.3862308292566522, -0.17551633097799743, 0,
+                                2.4392572425563213, 13.865339409688449, 28.536458135725827, 1
+                            ]
+                        }]);
+                    })
+                    .then(function() {
+                        viewer.resetCamera(0)
+                    });
+            }
+
+            function propset() {
+                viewer.clear()
+                    .then(function () {
+                        return viewer.loadPdbIds([{
+                            pdbId: '4HHB',
+                            props: {
+                                kind: 'prop-set',
+                                representation: [{
+                                    name: 'color',
+                                    positions: [{
+                                        label_asym_id: 'A',
+                                        label_seq_id: { beg: 4, end: 141 }
+                                    }],
+                                    value: 13203230
+                                }],
+                                selection: [{
+                                    label_asym_id: 'A',
+                                    label_seq_id: { beg: 4, end: 141 }
+                                }]
+                            }
+                        }, {
+                            pdbId: '1OJ6',
+                            props: {
+                                kind: 'prop-set',
+                                representation: [{
+                                    name: 'color',
+                                    positions: [{
+                                        label_asym_id: 'A',
+                                        label_seq_id: { beg: 3, end: 44 }
+                                    }, {
+                                        label_asym_id: 'A',
+                                        label_seq_id: { beg: 48, end: 49 }
+                                    }, {
+                                        label_asym_id: 'A',
+                                        label_seq_id: { beg: 54, end: 95 }
+                                    }, {
+                                        label_asym_id: 'A',
+                                        label_seq_id: { beg: 98, end: 149 }
+                                    }],
+                                    value: 4947916
+                                }],
+                                selection: [{
+                                    label_asym_id: 'A',
+                                    label_seq_id: { beg: 3, end: 44 }
+                                }, {
+                                    label_asym_id: 'A',
+                                    label_seq_id: { beg: 48, end: 49 }
+                                }, {
+                                    label_asym_id: 'A',
+                                    label_seq_id: { beg: 54, end: 95 }
+                                }, {
+                                    label_asym_id: 'A',
+                                    label_seq_id: { beg: 98, end: 149 }
+                                }]
+                            },
+                            matrix: [
+                                -0.6263111483773867, -0.38259812283613237, -0.6792297268380318, 0.0,
+                                0.3014888059527303, 0.6846150421427817, -0.6636314820418288, 0.0,
+                                0.7189150473480135, -0.6204199549290021, -0.313432982027257, 0.0,
+                                -30.829713890311414, 24.04442469172666, 55.437150645037654, 1.0
+                            ]
+                        }]);
+                    })
+                    .then(function () {
+                        viewer.resetCamera(0)
                     });
             }
         </script>

+ 6 - 4
src/viewer/ui/strucmotif.tsx

@@ -122,21 +122,23 @@ class SubmitControls extends PurePluginUIComponent<{}, { isBusy: boolean, residu
             const l = loci[i];
             const { structure, elements } = l.loci;
             pdbId.add(structure.model.entry);
+
+            // need to reverse here as Mol*, for some reason, inverts the order specified in the CIF file
+            const struct_oper_list_ids = StructureProperties.unit.pdbx_struct_oper_list_ids(location).reverse();
+            const struct_oper_id = struct_oper_list_ids?.length ? struct_oper_list_ids.join('x') : '1';
+
             // only first element and only first index will be considered (ignoring multiple residues)
             if (!determineBackboneAtom(structure, elements[0])) {
-                const struct_oper_list_ids = StructureProperties.unit.pdbx_struct_oper_list_ids(location);
-                const struct_oper_id = struct_oper_list_ids?.length ? struct_oper_list_ids.join('x') : '1';
                 alert(`No CA or C4' atom for ${StructureProperties.residue.label_seq_id(location)} | ${StructureProperties.chain.label_asym_id(location)} | ${struct_oper_id}`);
                 return;
             }
 
             // handle pure residue-info
-            const struct_oper_list_ids = StructureProperties.unit.pdbx_struct_oper_list_ids(location);
             // TODO honor NCS operators: StructureProperties.unit.struct_ncs_oper_id(location);
             const residueId = {
                 label_asym_id: StructureProperties.chain.label_asym_id(location),
                 // can be empty array if model is selected
-                struct_oper_id: struct_oper_list_ids?.length ? struct_oper_list_ids.join('x') : '1',
+                struct_oper_id,
                 label_seq_id: StructureProperties.residue.label_seq_id(location)
             };
             residueIds.push(residueId);