Explorar el Código

Assembly Symmetry extension customization (#950)

* Show assembly symmetry from PDBe API (quick and dirty)

* Treat 404 from PDBe API as success

* RCSBAssemblySymmetry extension: make defaults configurable via plugin config items

* RCSBAssemblySymmetry: revert configs to default values

* RCSBAssemblySymmetry: correctly handle non-assembly structure with PDBe API
midlik hace 1 año
padre
commit
65cad5ea4d

+ 7 - 0
src/apps/viewer/app.ts

@@ -51,6 +51,7 @@ import { Backgrounds } from '../../extensions/backgrounds';
 import { SbNcbrPartialCharges, SbNcbrPartialChargesPreset, SbNcbrPartialChargesPropertyProvider } from '../../extensions/sb-ncbr';
 import { wwPDBStructConnExtensionFunctions } from '../../extensions/wwpdb/struct-conn';
 import { wwPDBChemicalComponentDictionary } from '../../extensions/wwpdb/ccd/behavior';
+import { RCSBAssemblySymmetryConfig } from '../../extensions/rcsb/assembly-symmetry/behavior';
 
 export { PLUGIN_VERSION as version } from '../../mol-plugin/version';
 export { setDebugMode, setProductionMode, setTimingMode, consoleStats } from '../../mol-util/debug';
@@ -114,6 +115,9 @@ const DefaultViewerOptions = {
     emdbProvider: PluginConfig.Download.DefaultEmdbProvider.defaultValue,
     saccharideCompIdMapType: 'default' as SaccharideCompIdMapType,
     volumesAndSegmentationsDefaultServer: VolsegVolumeServerConfig.DefaultServer.defaultValue,
+    rcsbAssemblySymmetryDefaultServerType: RCSBAssemblySymmetryConfig.DefaultServerType.defaultValue,
+    rcsbAssemblySymmetryDefaultServerUrl: RCSBAssemblySymmetryConfig.DefaultServerUrl.defaultValue,
+    rcsbAssemblySymmetryApplyColors: RCSBAssemblySymmetryConfig.ApplyColors.defaultValue,
 };
 type ViewerOptions = typeof DefaultViewerOptions;
 
@@ -191,6 +195,9 @@ export class Viewer {
                 [PluginConfig.Structure.DefaultRepresentationPreset, ViewerAutoPreset.id],
                 [PluginConfig.Structure.SaccharideCompIdMapType, o.saccharideCompIdMapType],
                 [VolsegVolumeServerConfig.DefaultServer, o.volumesAndSegmentationsDefaultServer],
+                [RCSBAssemblySymmetryConfig.DefaultServerType, o.rcsbAssemblySymmetryDefaultServerType],
+                [RCSBAssemblySymmetryConfig.DefaultServerUrl, o.rcsbAssemblySymmetryDefaultServerUrl],
+                [RCSBAssemblySymmetryConfig.ApplyColors, o.rcsbAssemblySymmetryApplyColors],
             ]
         };
 

+ 41 - 14
src/extensions/rcsb/assembly-symmetry/behavior.ts

@@ -5,12 +5,13 @@
  */
 
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
-import { AssemblySymmetryProvider, AssemblySymmetry, AssemblySymmetryDataProvider } from './prop';
+import { AssemblySymmetryProvider, AssemblySymmetry, AssemblySymmetryDataProvider, AssemblySymmetryDataParams } from './prop';
 import { PluginBehavior } from '../../../mol-plugin/behavior/behavior';
 import { AssemblySymmetryParams, AssemblySymmetryRepresentation } from './representation';
 import { AssemblySymmetryClusterColorThemeProvider } from './color';
 import { PluginStateTransform, PluginStateObject } from '../../../mol-plugin-state/objects';
 import { Task } from '../../../mol-task';
+import { PluginConfigItem } from '../../../mol-plugin/config';
 import { PluginContext } from '../../../mol-plugin/context';
 import { StateTransformer, StateAction, StateObject, StateTransform, StateObjectRef } from '../../../mol-state';
 import { GenericRepresentationRef } from '../../../mol-plugin-state/manager/structure/hierarchy-state';
@@ -77,14 +78,15 @@ export const InitAssemblySymmetry3D = StateAction.build({
         description: 'Initialize Assembly Symmetry axes and cage. Data calculated with BioJava, obtained via RCSB PDB.'
     },
     from: PluginStateObject.Molecule.Structure,
-    isApplicable: (a) => AssemblySymmetry.isApplicable(a.data)
-})(({ a, ref, state }, plugin: PluginContext) => Task.create('Init Assembly Symmetry', async ctx => {
+    isApplicable: (a) => AssemblySymmetry.isApplicable(a.data),
+    params: (a, plugin: PluginContext) => getConfiguredDefaultParams(plugin)
+})(({ a, ref, state, params }, plugin: PluginContext) => Task.create('Init Assembly Symmetry', async ctx => {
     try {
         const propCtx = { runtime: ctx, assetManager: plugin.managers.asset };
-        await AssemblySymmetryDataProvider.attach(propCtx, a.data);
+        await AssemblySymmetryDataProvider.attach(propCtx, a.data, params);
         const assemblySymmetryData = AssemblySymmetryDataProvider.get(a.data).value;
         const symmetryIndex = assemblySymmetryData ? AssemblySymmetry.firstNonC1(assemblySymmetryData) : -1;
-        await AssemblySymmetryProvider.attach(propCtx, a.data, { symmetryIndex });
+        await AssemblySymmetryProvider.attach(propCtx, a.data, { ...params, symmetryIndex });
     } catch (e) {
         plugin.log.error(`Assembly Symmetry: ${e}`);
         return;
@@ -152,10 +154,6 @@ const AssemblySymmetry3D = PluginStateTransform.BuiltIn({
 
 //
 
-export const AssemblySymmetryPresetParams = {
-    ...StructureRepresentationPresetProvider.CommonParams,
-};
-
 export const AssemblySymmetryPreset = StructureRepresentationPresetProvider({
     id: 'preset-structure-representation-rcsb-assembly-symmetry',
     display: {
@@ -165,7 +163,12 @@ export const AssemblySymmetryPreset = StructureRepresentationPresetProvider({
     isApplicable(a) {
         return AssemblySymmetry.isApplicable(a.data);
     },
-    params: () => AssemblySymmetryPresetParams,
+    params: (a, plugin) => {
+        return {
+            ...StructureRepresentationPresetProvider.CommonParams,
+            ...getConfiguredDefaultParams(plugin)
+        };
+    },
     async apply(ref, params, plugin) {
         const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
         const structure = structureCell?.obj?.data;
@@ -174,15 +177,16 @@ export const AssemblySymmetryPreset = StructureRepresentationPresetProvider({
         if (!AssemblySymmetryDataProvider.get(structure).value) {
             await plugin.runTask(Task.create('Assembly Symmetry', async runtime => {
                 const propCtx = { runtime, assetManager: plugin.managers.asset };
-                await AssemblySymmetryDataProvider.attach(propCtx, structure);
+                const propProps = { serverType: params.serverType, serverUrl: params.serverUrl };
+                await AssemblySymmetryDataProvider.attach(propCtx, structure, propProps);
                 const assemblySymmetryData = AssemblySymmetryDataProvider.get(structure).value;
                 const symmetryIndex = assemblySymmetryData ? AssemblySymmetry.firstNonC1(assemblySymmetryData) : -1;
-                await AssemblySymmetryProvider.attach(propCtx, structure, { symmetryIndex });
+                await AssemblySymmetryProvider.attach(propCtx, structure, { ...propProps, symmetryIndex });
             }));
         }
 
         const assemblySymmetry = await tryCreateAssemblySymmetry(plugin, structureCell);
-        const colorTheme = assemblySymmetry.isOk ? Tag.Cluster as any : undefined;
+        const colorTheme = getRCSBAssemblySymmetryConfig(plugin).ApplyColors && assemblySymmetry.isOk ? Tag.Cluster as any : undefined;
         const preset = await PresetStructureRepresentations.auto.apply(ref, { ...params, theme: { globalName: colorTheme, focus: { name: colorTheme } } }, plugin);
 
         return { components: preset.components, representations: { ...preset.representations, assemblySymmetry } };
@@ -194,4 +198,27 @@ export function tryCreateAssemblySymmetry(plugin: PluginContext, structure: Stat
     const assemblySymmetry = state.build().to(structure)
         .applyOrUpdateTagged(AssemblySymmetry.Tag.Representation, AssemblySymmetry3D, params, { state: initialState });
     return assemblySymmetry.commit({ revertOnError: true });
-}
+}
+
+//
+
+export const RCSBAssemblySymmetryConfig = {
+    DefaultServerType: new PluginConfigItem('rcsb-assembly-symmetry.server-type', AssemblySymmetryDataParams.serverType.defaultValue),
+    DefaultServerUrl: new PluginConfigItem('rcsb-assembly-symmetry.server-url', AssemblySymmetryDataParams.serverUrl.defaultValue),
+    ApplyColors: new PluginConfigItem('rcsb-assembly-symmetry.apply-colors', true),
+};
+
+export function getRCSBAssemblySymmetryConfig(plugin: PluginContext): { [key in keyof typeof RCSBAssemblySymmetryConfig]: NonNullable<typeof RCSBAssemblySymmetryConfig[key]['defaultValue']> } {
+    return {
+        ApplyColors: plugin.config.get(RCSBAssemblySymmetryConfig.ApplyColors) ?? RCSBAssemblySymmetryConfig.ApplyColors.defaultValue ?? true,
+        DefaultServerType: plugin.config.get(RCSBAssemblySymmetryConfig.DefaultServerType) ?? RCSBAssemblySymmetryConfig.DefaultServerType.defaultValue ?? AssemblySymmetryDataParams.serverType.defaultValue,
+        DefaultServerUrl: plugin.config.get(RCSBAssemblySymmetryConfig.DefaultServerUrl) ?? RCSBAssemblySymmetryConfig.DefaultServerUrl.defaultValue ?? AssemblySymmetryDataParams.serverUrl.defaultValue,
+    };
+}
+
+function getConfiguredDefaultParams(plugin: PluginContext) {
+    const config = getRCSBAssemblySymmetryConfig(plugin);
+    const params = PD.clone(AssemblySymmetryDataParams);
+    PD.setDefaultValues(params, { serverType: config.DefaultServerType, serverUrl: config.DefaultServerUrl });
+    return params;
+}

+ 38 - 3
src/extensions/rcsb/assembly-symmetry/prop.ts

@@ -20,6 +20,7 @@ import { SetUtils } from '../../../mol-util/set';
 import { MolScriptBuilder as MS } from '../../../mol-script/language/builder';
 import { compile } from '../../../mol-script/runtime/query/compiler';
 import { CustomPropertyDescriptor } from '../../../mol-model/custom-property';
+import { Asset } from '../../../mol-util/assets';
 
 const BiologicalAssemblyNames = new Set([
     'author_and_software_defined_assembly',
@@ -48,7 +49,7 @@ export namespace AssemblySymmetry {
         Representation = 'rcsb-assembly-symmetry-3d'
     }
 
-    export const DefaultServerUrl = 'https://data.rcsb.org/graphql';
+    export const DefaultServerUrl = 'https://data.rcsb.org/graphql'; // Alternative: 'https://www.ebi.ac.uk/pdbe/aggregated-api/pdb/symmetry' (if serverType is 'pdbe')
 
     export function isApplicable(structure?: Structure): boolean {
         return (
@@ -61,6 +62,8 @@ export namespace AssemblySymmetry {
     export async function fetch(ctx: CustomProperty.Context, structure: Structure, props: AssemblySymmetryDataProps): Promise<CustomProperty.Data<AssemblySymmetryDataValue>> {
         if (!isApplicable(structure)) return { value: [] };
 
+        if (props.serverType === 'pdbe') return fetch_PDBe(ctx, structure, props);
+
         const client = new GraphQLClient(props.serverUrl, ctx.assetManager);
         const variables: AssemblySymmetryQueryVariables = {
             assembly_id: structure.units[0].conformation.operator.assembly?.id || '',
@@ -77,6 +80,37 @@ export namespace AssemblySymmetry {
         return { value, assets: [result] };
     }
 
+    async function fetch_PDBe(ctx: CustomProperty.Context, structure: Structure, props: AssemblySymmetryDataProps): Promise<CustomProperty.Data<AssemblySymmetryDataValue>> {
+        const assembly_id = structure.units[0].conformation.operator.assembly?.id || '-1'; // should use '' instead of '-1' but the API does not support non-number assembly_id
+        const entry_id = structure.units[0].model.entryId.toLowerCase();
+        const url = `${props.serverUrl}/${entry_id}?assembly_id=${assembly_id}`;
+        const asset = Asset.getUrlAsset(ctx.assetManager, url);
+        let dataWrapper: Asset.Wrapper<'json'>;
+        try {
+            dataWrapper = await ctx.assetManager.resolve(asset, 'json').runInContext(ctx.runtime);
+        } catch (err) {
+            // PDBe API returns 404 when there are no symmetries -> treat as success with empty json in body
+            if (`${err}`.includes('404')) { // dirrrty
+                dataWrapper = Asset.Wrapper({}, asset, ctx.assetManager);
+            } else {
+                throw err;
+            }
+        }
+        const data = dataWrapper.data;
+
+        const value: AssemblySymmetryDataValue = (data[entry_id] ?? []).map((v: any) => ({
+            kind: 'Global Symmetry',
+            oligomeric_state: v.oligomeric_state,
+            stoichiometry: [v.stoichiometry],
+            symbol: v.symbol,
+            type: v.type,
+            clusters: [],
+            rotation_axes: v.rotation_axes,
+        }));
+
+        return { value, assets: [dataWrapper] };
+    }
+
     /** Returns the index of the first non C1 symmetry or -1 */
     export function firstNonC1(assemblySymmetryData: AssemblySymmetryDataValue) {
         for (let i = 0, il = assemblySymmetryData.length; i < il; ++i) {
@@ -147,7 +181,8 @@ export function getSymmetrySelectParam(structure?: Structure) {
 //
 
 export const AssemblySymmetryDataParams = {
-    serverUrl: PD.Text(AssemblySymmetry.DefaultServerUrl, { description: 'GraphQL endpoint URL' })
+    serverType: PD.Select('rcsb', [['rcsb', 'RCSB'], ['pdbe', 'PDBe']] as const),
+    serverUrl: PD.Text(AssemblySymmetry.DefaultServerUrl, { description: 'GraphQL endpoint URL (if server type is RCSB) or PDBe API endpoint URL (if server type is PDBe)' })
 };
 export type AssemblySymmetryDataParams = typeof AssemblySymmetryDataParams
 export type AssemblySymmetryDataProps = PD.Values<AssemblySymmetryDataParams>
@@ -174,7 +209,7 @@ export const AssemblySymmetryDataProvider: CustomStructureProperty.Provider<Asse
 
 function getAssemblySymmetryParams(data?: Structure) {
     return {
-        ... AssemblySymmetryDataParams,
+        ...AssemblySymmetryDataParams,
         symmetryIndex: getSymmetrySelectParam(data)
     };
 }

+ 8 - 3
src/extensions/rcsb/assembly-symmetry/ui.tsx

@@ -6,7 +6,7 @@
 
 import { CollapsableState, CollapsableControls } from '../../../mol-plugin-ui/base';
 import { ApplyActionControl } from '../../../mol-plugin-ui/state/apply-action';
-import { InitAssemblySymmetry3D, AssemblySymmetry3D, AssemblySymmetryPreset, tryCreateAssemblySymmetry } from './behavior';
+import { InitAssemblySymmetry3D, AssemblySymmetry3D, AssemblySymmetryPreset, tryCreateAssemblySymmetry, getRCSBAssemblySymmetryConfig } from './behavior';
 import { AssemblySymmetryProvider, AssemblySymmetryProps, AssemblySymmetryDataProvider, AssemblySymmetry } from './prop';
 import { ParameterControls } from '../../../mol-plugin-ui/controls/parameters';
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
@@ -72,6 +72,7 @@ export class AssemblySymmetryControls extends CollapsableControls<{}, AssemblySy
     get params() {
         const structure = this.pivot.cell.obj?.data;
         const params = PD.clone(structure ? AssemblySymmetryProvider.getParams(structure) : AssemblySymmetryProvider.defaultParams);
+        params.serverType.isHidden = true;
         params.serverUrl.isHidden = true;
         return params;
     }
@@ -111,7 +112,9 @@ export class AssemblySymmetryControls extends CollapsableControls<{}, AssemblySy
                 }
             } else {
                 tryCreateAssemblySymmetry(this.plugin, s.cell);
-                await this.plugin.managers.structure.component.updateRepresentationsTheme(components, { color: AssemblySymmetry.Tag.Cluster as any });
+                if (getRCSBAssemblySymmetryConfig(this.plugin).ApplyColors) {
+                    await this.plugin.managers.structure.component.updateRepresentationsTheme(components, { color: AssemblySymmetry.Tag.Cluster as any });
+                }
             }
         }
     }
@@ -151,5 +154,7 @@ export class AssemblySymmetryControls extends CollapsableControls<{}, AssemblySy
 const EnableAssemblySymmetry3D = StateAction.build({
     from: PluginStateObject.Molecule.Structure,
 })(({ a, ref, state }, plugin: PluginContext) => Task.create('Enable Assembly Symmetry', async ctx => {
-    await AssemblySymmetryPreset.apply(ref, Object.create(null), plugin);
+    const presetParams = AssemblySymmetryPreset.params?.(a, plugin) as PD.Params | undefined;
+    const presetProps = presetParams ? PD.getDefaultValues(presetParams) : Object.create(null);
+    await AssemblySymmetryPreset.apply(ref, presetProps, plugin);
 }));