Переглянути джерело

support for recommended iso value volumes

- automaticlly get for volumes with emdb entryId
Alexander Rose 4 роки тому
батько
коміт
94cd5d3395

+ 1 - 22
src/apps/viewer/embedded.html

@@ -36,28 +36,7 @@
                 emdbProvider: 'rcsb',
             });
             viewer.loadPdb('7bv2');
-            viewer.loadEmdb('EMD-30210');
-
-            // TODO add Volume.customProperty and load suggested isoValue via custom property
-            var sub = viewer.plugin.managers.volume.hierarchy.behaviors.selection.subscribe(function (value) {
-                if (value.volume?.representations[0]) {
-                    var ref = value.volume.representations[0].cell;
-                    var tree = viewer.plugin.state.data.build().to(ref).update({
-                        type: {
-                            name: 'isosurface',
-                            params: {
-                                isoValue: {
-                                    kind: 'relative',
-                                    relativeValue: 6
-                                }
-                            }
-                        },
-                        colorTheme: ref.transform.params?.colorTheme
-                    });
-                    viewer.plugin.runTask(viewer.plugin.state.data.updateTree(tree));
-                    if (typeof sub !== 'undefined') sub.unsubscribe();
-                }
-            });
+            viewer.loadEmdb('EMD-30210', { detail: 6 });
         </script>
     </body>
 </html>

+ 2 - 2
src/apps/viewer/index.ts

@@ -181,7 +181,7 @@ export class Viewer {
         }));
     }
 
-    loadEmdb(emdb: string) {
+    loadEmdb(emdb: string, options?: { detail?: number }) {
         const provider = this.plugin.config.get(PluginConfig.Download.DefaultEmdbProvider)!;
         return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadDensity, {
             source: {
@@ -191,7 +191,7 @@ export class Viewer {
                         id: emdb,
                         server: provider,
                     },
-                    detail: 3,
+                    detail: options?.detail ?? 3,
                 }
             }
         }));

+ 1 - 1
src/mol-model-formats/structure/common/property.ts

@@ -32,7 +32,7 @@ class FormatRegistry<T> {
     }
 }
 
-export { FormatPropertyProvider as FormatPropertyProvider };
+export { FormatPropertyProvider };
 
 interface FormatPropertyProvider<T> {
     readonly descriptor: CustomPropertyDescriptor

+ 2 - 1
src/mol-model-formats/volume/ccp4.ts

@@ -40,7 +40,7 @@ function getTypedArrayCtor(header: Ccp4Header) {
     throw Error(`${valueType} is not a supported value format.`);
 }
 
-export function volumeFromCcp4(source: Ccp4File, params?: { voxelSize?: Vec3, offset?: Vec3, label?: string }): Task<Volume> {
+export function volumeFromCcp4(source: Ccp4File, params?: { voxelSize?: Vec3, offset?: Vec3, label?: string, entryId?: string }): Task<Volume> {
     return Task.create<Volume>('Create Volume', async ctx => {
         const { header, values } = source;
         const size = Vec3.create(header.xLength, header.yLength, header.zLength);
@@ -73,6 +73,7 @@ export function volumeFromCcp4(source: Ccp4File, params?: { voxelSize?: Vec3, of
 
         return {
             label: params?.label,
+            entryId: params?.entryId,
             grid: {
                 transform: { kind: 'spacegroup', cell, fractionalBox: Box3D.create(origin_frac, Vec3.add(Vec3.zero(), origin_frac, dimensions_frac)) },
                 cells: data,

+ 2 - 1
src/mol-model-formats/volume/cube.ts

@@ -12,7 +12,7 @@ import { arrayMax, arrayMean, arrayMin, arrayRms } from '../../mol-util/array';
 import { ModelFormat } from '../format';
 import { CustomProperties } from '../../mol-model/custom-property';
 
-export function volumeFromCube(source: CubeFile, params?: { dataIndex?: number, label?: string }): Task<Volume> {
+export function volumeFromCube(source: CubeFile, params?: { dataIndex?: number, label?: string, entryId?: string }): Task<Volume> {
     return Task.create<Volume>('Create Volume', async () => {
         const { header, values: sourceValues } = source;
         const space = Tensor.Space(header.dim, [0, 1, 2], Float64Array);
@@ -46,6 +46,7 @@ export function volumeFromCube(source: CubeFile, params?: { dataIndex?: number,
 
         return {
             label: params?.label,
+            entryId: params?.entryId,
             grid: {
                 transform: { kind: 'matrix', matrix },
                 cells: data,

+ 3 - 1
src/mol-model-formats/volume/density-server.ts

@@ -12,7 +12,7 @@ import { Tensor, Vec3 } from '../../mol-math/linear-algebra';
 import { ModelFormat } from '../format';
 import { CustomProperties } from '../../mol-model/custom-property';
 
-export function volumeFromDensityServerData(source: DensityServer_Data_Database): Task<Volume> {
+export function volumeFromDensityServerData(source: DensityServer_Data_Database, params?: Partial<{ label: string, entryId: string }>): Task<Volume> {
     return Task.create<Volume>('Create Volume', async ctx => {
         const { volume_data_3d_info: info, volume_data_3d: values } = source;
         const cell = SpacegroupCell.create(
@@ -36,6 +36,8 @@ export function volumeFromDensityServerData(source: DensityServer_Data_Database)
         const dimensions = Vec3.ofArray(normalizeOrder(info.dimensions.value(0)));
 
         return {
+            label: params?.label,
+            entryId: params?.entryId,
             grid: {
                 transform: { kind: 'spacegroup', cell, fractionalBox: Box3D.create(origin, Vec3.add(Vec3.zero(), origin, dimensions)) },
                 cells: data,

+ 2 - 1
src/mol-model-formats/volume/dsn6.ts

@@ -14,7 +14,7 @@ import { arrayMin, arrayMax, arrayMean, arrayRms } from '../../mol-util/array';
 import { ModelFormat } from '../format';
 import { CustomProperties } from '../../mol-model/custom-property';
 
-export function volumeFromDsn6(source: Dsn6File, params?: { voxelSize?: Vec3, label?: string }): Task<Volume> {
+export function volumeFromDsn6(source: Dsn6File, params?: { voxelSize?: Vec3, label?: string, entryId?: string }): Task<Volume> {
     return Task.create<Volume>('Create Volume', async ctx => {
         const { header, values } = source;
         const size = Vec3.create(header.xlen, header.ylen, header.zlen);
@@ -35,6 +35,7 @@ export function volumeFromDsn6(source: Dsn6File, params?: { voxelSize?: Vec3, la
 
         return {
             label: params?.label,
+            entryId: params?.entryId,
             grid: {
                 transform: { kind: 'spacegroup', cell, fractionalBox: Box3D.create(origin_frac, Vec3.add(Vec3.zero(), origin_frac, dimensions_frac)) },
                 cells: data,

+ 2 - 1
src/mol-model-formats/volume/dx.ts

@@ -12,7 +12,7 @@ import { arrayMax, arrayMean, arrayMin, arrayRms } from '../../mol-util/array';
 import { ModelFormat } from '../format';
 import { CustomProperties } from '../../mol-model/custom-property';
 
-export function volumeFromDx(source: DxFile, params?: { label?: string }): Task<Volume> {
+export function volumeFromDx(source: DxFile, params?: { label?: string, entryId?: string }): Task<Volume> {
     return Task.create<Volume>('Create Volume', async () => {
         const { header, values } = source;
         const space = Tensor.Space(header.dim, [0, 1, 2], Float64Array);
@@ -23,6 +23,7 @@ export function volumeFromDx(source: DxFile, params?: { label?: string }): Task<
 
         return {
             label: params?.label,
+            entryId: params?.entryId,
             grid: {
                 transform: { kind: 'matrix', matrix },
                 cells: data,

+ 47 - 0
src/mol-model-formats/volume/property.ts

@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { CustomPropertyDescriptor } from '../../mol-model/custom-property';
+import { Volume } from '../../mol-model/volume/volume';
+
+export { PropertyProvider };
+
+interface PropertyProvider<T> {
+    readonly descriptor: CustomPropertyDescriptor
+    get(volume: Volume): T | undefined
+    set(volume: Volume, value: T): void
+}
+
+namespace PropertyProvider {
+    export function create<T>(descriptor: CustomPropertyDescriptor): PropertyProvider<T> {
+        const { name } = descriptor;
+
+        return {
+            descriptor,
+            get(volume: Volume): T | undefined {
+                return volume._propertyData[name];
+            },
+            set(volume: Volume, value: T) {
+                volume.customProperties.add(descriptor);
+                volume._propertyData[name] = value;
+            }
+        };
+    }
+}
+
+//
+
+export { RecommendedIsoValue };
+
+type RecommendedIsoValue = Volume.IsoValue
+
+namespace RecommendedIsoValue {
+    export const Descriptor: CustomPropertyDescriptor = {
+        name: 'recommended_iso_value',
+    };
+
+    export const Provider = PropertyProvider.create<RecommendedIsoValue>(Descriptor);
+}

+ 1 - 0
src/mol-model/volume/volume.ts

@@ -16,6 +16,7 @@ import { CustomProperties } from '../custom-property';
 
 export interface Volume {
     readonly label?: string
+    readonly entryId?: string,
     readonly grid: Grid
     readonly sourceData: ModelFormat
 

+ 6 - 3
src/mol-plugin-state/actions/volume.ts

@@ -39,14 +39,14 @@ const DownloadDensity = StateAction.build({
                         id: PD.Text('1tqn', { label: 'Id' }),
                         server: PD.Select('pdbe', [['pdbe', 'PDBe'], ['rcsb', 'RCSB PDB']]),
                     }, { pivot: 'id' }),
-                    detail: PD.Numeric(3, { min: 0, max: 10, step: 1 }, { label: 'Detail' }),
+                    detail: PD.Numeric(3, { min: 0, max: 6, step: 1 }, { label: 'Detail' }),
                 }, { isFlat: true }),
                 'pdb-emd-ds': PD.Group({
                     provider: PD.Group({
                         id: PD.Text('emd-8004', { label: 'Id' }),
                         server: PD.Select<EmdbDownloadProvider>('pdbe', [['pdbe', 'PDBe'], ['rcsb', 'RCSB PDB']]),
                     }, { pivot: 'id' }),
-                    detail: PD.Numeric(3, { min: 0, max: 10, step: 1 }, { label: 'Detail' }),
+                    detail: PD.Numeric(3, { min: 0, max: 6, step: 1 }, { label: 'Detail' }),
                 }, { isFlat: true }),
                 'url': PD.Group({
                     url: PD.Url(''),
@@ -113,6 +113,7 @@ const DownloadDensity = StateAction.build({
     }
 
     const data = await plugin.builders.data.download(downloadParams);
+    let entryId: string | undefined = undefined;
 
     switch (src.name) {
         case 'url':
@@ -120,12 +121,14 @@ const DownloadDensity = StateAction.build({
             provider = src.params.format === 'auto' ? plugin.dataFormats.auto(getFileInfo(Asset.getUrl(downloadParams.url)), data.cell?.obj!) : plugin.dataFormats.get(src.params.format);
             break;
         case 'pdb-xray':
+            entryId = src.params.provider.id;
             provider = src.params.provider.server === 'pdbe'
                 ? plugin.dataFormats.get('ccp4')
                 : plugin.dataFormats.get('dsn6');
             break;
         case 'pdb-emd-ds':
         case 'pdb-xray-ds':
+            entryId = src.params.provider.id;
             provider = plugin.dataFormats.get('dscif');
             break;
         default: throw new Error(`${(src as any).name} not supported.`);
@@ -136,7 +139,7 @@ const DownloadDensity = StateAction.build({
         return;
     }
 
-    const volumes = await provider.parse(plugin, data);
+    const volumes = await provider.parse(plugin, data, { entryId });
     await provider.visuals?.(plugin, volumes);
 }));
 

+ 66 - 14
src/mol-plugin-state/formats/volume.ts

@@ -15,27 +15,73 @@ import { ColorNames } from '../../mol-util/color/names';
 import { Volume } from '../../mol-model/volume';
 import { createVolumeRepresentationParams } from '../helpers/volume-representation-params';
 import { objectForEach } from '../../mol-util/object';
+import { RecommendedIsoValue } from '../../mol-model-formats/volume/property';
+import { getContourLevelEmdb } from '../../mol-plugin/behavior/dynamic/volume-streaming/util';
+import { Task } from '../../mol-task';
+import { DscifFormat } from '../../mol-model-formats/volume/density-server';
 
 const Category = 'Volume';
+type Params = { entryId?: string };
+
+async function tryObtainRecommendedIsoValue(plugin: PluginContext, volume?: Volume) {
+    if (!volume) return;
+
+    const { entryId } = volume;
+    if (!entryId || !entryId.toLowerCase().startsWith('emd')) return;
+
+    return plugin.runTask(Task.create('Try Set Recommended IsoValue', async ctx => {
+        try {
+            const absIsoLevel = await getContourLevelEmdb(plugin, ctx, entryId);
+            RecommendedIsoValue.Provider.set(volume, Volume.IsoValue.absolute(absIsoLevel));
+        } catch (e) { }
+    }));
+}
+
+function tryGetRecomendedIsoValue(volume: Volume) {
+    const recommendedIsoValue = RecommendedIsoValue.Provider.get(volume);
+    if (!recommendedIsoValue) return;
+
+    if (recommendedIsoValue.kind === 'relative') return recommendedIsoValue;
+
+    let stats = volume.grid.stats;
+    if (DscifFormat.is(volume.sourceData)) {
+        stats = {
+            min: volume.sourceData.data.volume_data_3d_info.min_source.value(0),
+            max: volume.sourceData.data.volume_data_3d_info.max_source.value(0),
+            mean: volume.sourceData.data.volume_data_3d_info.mean_source.value(0),
+            sigma: volume.sourceData.data.volume_data_3d_info.sigma_source.value(0),
+        };
+    }
+    return Volume.IsoValue.toRelative(recommendedIsoValue, stats);
+}
 
 async function defaultVisuals(plugin: PluginContext, data: { volume: StateObjectSelector<PluginStateObject.Volume.Data> }) {
-    const visual = plugin.build().to(data.volume).apply(StateTransforms.Representation.VolumeRepresentation3D);
+
+    const typeParams: { isoValue?: Volume.IsoValue } = {};
+    const isoValue = data.volume.data && tryGetRecomendedIsoValue(data.volume.data);
+    if (isoValue) typeParams.isoValue = isoValue;
+
+    const visual = plugin.build().to(data.volume).apply(StateTransforms.Representation.VolumeRepresentation3D, createVolumeRepresentationParams(plugin, data.volume.data, {
+        type: 'isosurface',
+        typeParams,
+    }));
     return [await visual.commit()];
 }
 
 export const Ccp4Provider = DataFormatProvider({
-    label: 'CCP4/MRC/BRIX',
-    description: 'CCP4/MRC/BRIX',
+    label: 'CCP4/MRC/MAP',
+    description: 'CCP4/MRC/MAP',
     category: Category,
     binaryExtensions: ['ccp4', 'mrc', 'map'],
-    parse: async (plugin, data) => {
+    parse: async (plugin, data, params?: Params) => {
         const format = plugin.build()
             .to(data)
             .apply(StateTransforms.Data.ParseCcp4, {}, { state: { isGhost: true } });
 
-        const volume = format.apply(StateTransforms.Volume.VolumeFromCcp4);
+        const volume = format.apply(StateTransforms.Volume.VolumeFromCcp4, { entryId: params?.entryId });
 
         await format.commit({ revertOnError: true });
+        await tryObtainRecommendedIsoValue(plugin, volume.selector.data);
 
         return { format: format.selector, volume: volume.selector };
     },
@@ -47,14 +93,15 @@ export const Dsn6Provider = DataFormatProvider({
     description: 'DSN6/BRIX',
     category: Category,
     binaryExtensions: ['dsn6', 'brix'],
-    parse: async (plugin, data) => {
+    parse: async (plugin, data, params?: Params) => {
         const format = plugin.build()
             .to(data)
             .apply(StateTransforms.Data.ParseDsn6, {}, { state: { isGhost: true } });
 
-        const volume = format.apply(StateTransforms.Volume.VolumeFromDsn6);
+        const volume = format.apply(StateTransforms.Volume.VolumeFromDsn6, { entryId: params?.entryId });
 
         await format.commit({ revertOnError: true });
+        await tryObtainRecommendedIsoValue(plugin, volume.selector.data);
 
         return { format: format.selector, volume: volume.selector };
     },
@@ -67,14 +114,15 @@ export const DxProvider = DataFormatProvider({
     category: Category,
     stringExtensions: ['dx'],
     binaryExtensions: ['dxbin'],
-    parse: async (plugin, data) => {
+    parse: async (plugin, data, params?: Params) => {
         const format = plugin.build()
             .to(data)
             .apply(StateTransforms.Data.ParseDx, {}, { state: { isGhost: true } });
 
-        const volume = format.apply(StateTransforms.Volume.VolumeFromDx);
+        const volume = format.apply(StateTransforms.Volume.VolumeFromDx, { entryId: params?.entryId });
 
         await volume.commit({ revertOnError: true });
+        await tryObtainRecommendedIsoValue(plugin, volume.selector.data);
 
         return { volume: volume.selector };
     },
@@ -86,18 +134,19 @@ export const CubeProvider = DataFormatProvider({
     description: 'Cube',
     category: Category,
     stringExtensions: ['cub', 'cube'],
-    parse: async (plugin, data) => {
+    parse: async (plugin, data, params?: Params) => {
         const format = plugin.build()
             .to(data)
             .apply(StateTransforms.Data.ParseCube, {}, { state: { isGhost: true } });
 
-        const volume = format.apply(StateTransforms.Volume.VolumeFromCube);
+        const volume = format.apply(StateTransforms.Volume.VolumeFromCube, { entryId: params?.entryId });
         const structure = format
             .apply(StateTransforms.Model.TrajectoryFromCube, void 0, { state: { isGhost: true } })
             .apply(StateTransforms.Model.ModelFromTrajectory)
             .apply(StateTransforms.Model.StructureFromModel);
 
         await format.commit({ revertOnError: true });
+        await tryObtainRecommendedIsoValue(plugin, volume.selector.data);
 
         return { format: format.selector, volume: volume.selector, structure: structure.selector };
     },
@@ -151,7 +200,7 @@ export const DscifProvider = DataFormatProvider({
     isApplicable: (info, data) => {
         return guessCifVariant(info, data) === 'dscif' ? true : false;
     },
-    parse: async (plugin, data) => {
+    parse: async (plugin, data, params?: Params) => {
         const cifCell = await plugin.build().to(data).apply(StateTransforms.Data.ParseCif).commit();
         const b = plugin.build().to(cifCell);
         const blocks = cifCell.obj!.data.blocks.slice(1); // zero block contains query meta-data
@@ -160,10 +209,11 @@ export const DscifProvider = DataFormatProvider({
 
         const volumes: StateObjectSelector<PluginStateObject.Volume.Data>[] = [];
         for (const block of blocks) {
-            volumes.push(b.apply(StateTransforms.Volume.VolumeFromDensityServerCif, { blockHeader: block.header }).selector);
+            volumes.push(b.apply(StateTransforms.Volume.VolumeFromDensityServerCif, { blockHeader: block.header, entryId: params?.entryId }).selector);
         }
 
         await b.commit();
+        for (const v of volumes) await tryObtainRecommendedIsoValue(plugin, v.data);
 
         return { volumes };
     },
@@ -173,9 +223,11 @@ export const DscifProvider = DataFormatProvider({
         const visuals: StateObjectSelector<PluginStateObject.Volume.Representation3D>[] = [];
 
         if (volumes.length > 0) {
+            const isoValue = (volumes[0].data && tryGetRecomendedIsoValue(volumes[0].data)) || Volume.IsoValue.relative(1.5);
+
             visuals[0] = tree
                 .to(volumes[0])
-                .apply(StateTransforms.Representation.VolumeRepresentation3D, VolumeRepresentation3DHelpers.getDefaultParamsStatic(plugin, 'isosurface', { isoValue: Volume.IsoValue.relative(1.5), alpha: 1 }, 'uniform', { value: ColorNames.teal }))
+                .apply(StateTransforms.Representation.VolumeRepresentation3D, VolumeRepresentation3DHelpers.getDefaultParamsStatic(plugin, 'isosurface', { isoValue, alpha: 1 }, 'uniform', { value: ColorNames.teal }))
                 .selector;
         }
 

+ 12 - 8
src/mol-plugin-state/transforms/volume.ts

@@ -35,7 +35,8 @@ const VolumeFromCcp4 = PluginStateTransform.BuiltIn({
     params(a) {
         return {
             voxelSize: PD.Vec3(Vec3.create(1, 1, 1)),
-            offset: PD.Vec3(Vec3.create(0, 0, 0))
+            offset: PD.Vec3(Vec3.create(0, 0, 0)),
+            entryId: PD.Text(''),
         };
     }
 })({
@@ -56,7 +57,8 @@ const VolumeFromDsn6 = PluginStateTransform.BuiltIn({
     to: SO.Volume.Data,
     params(a) {
         return {
-            voxelSize: PD.Vec3(Vec3.create(1, 1, 1))
+            voxelSize: PD.Vec3(Vec3.create(1, 1, 1)),
+            entryId: PD.Text(''),
         };
     }
 })({
@@ -76,9 +78,10 @@ const VolumeFromCube = PluginStateTransform.BuiltIn({
     from: SO.Format.Cube,
     to: SO.Volume.Data,
     params(a) {
-        if (!a) return { dataIndex: PD.Numeric(0) };
+        const dataIndex = a ? PD.Select(0, a.data.header.dataSetIds.map((id, i) => [i, `${id}`] as const)) : PD.Numeric(0);
         return {
-            dataIndex: PD.Select(0, a.data.header.dataSetIds.map((id, i) => [i, `${id}`] as const))
+            dataIndex,
+            entryId: PD.Text(''),
         };
     }
 })({
@@ -100,7 +103,6 @@ const VolumeFromDx = PluginStateTransform.BuiltIn({
 })({
     apply({ a }) {
         return Task.create('Parse DX', async ctx => {
-            console.log(a);
             const volume = await volumeFromDx(a.data, { label: a.data.name || a.label }).runInContext(ctx);
             const props = { label: volume.label || 'Volume', description: `Volume ${a.data.header.dim[0]}\u00D7${a.data.header.dim[1]}\u00D7${a.data.header.dim[2]}` };
             return new SO.Volume.Data(volume, props);
@@ -117,12 +119,14 @@ const VolumeFromDensityServerCif = PluginStateTransform.BuiltIn({
     params(a) {
         if (!a) {
             return {
-                blockHeader: PD.Optional(PD.Text(void 0, { description: 'Header of the block to parse. If none is specifed, the 1st data block in the file is used.' }))
+                blockHeader: PD.Optional(PD.Text(void 0, { description: 'Header of the block to parse. If none is specifed, the 1st data block in the file is used.' })),
+                entryId: PD.Text(''),
             };
         }
         const blocks = a.data.blocks.slice(1); // zero block contains query meta-data
         return {
-            blockHeader: PD.Optional(PD.Select(blocks[0] && blocks[0].header, blocks.map(b => [b.header, b.header] as [string, string]), { description: 'Header of the block to parse' }))
+            blockHeader: PD.Optional(PD.Select(blocks[0] && blocks[0].header, blocks.map(b => [b.header, b.header] as [string, string]), { description: 'Header of the block to parse' })),
+            entryId: PD.Text(''),
         };
     }
 })({
@@ -133,7 +137,7 @@ const VolumeFromDensityServerCif = PluginStateTransform.BuiltIn({
             const block = a.data.blocks.find(b => b.header === header);
             if (!block) throw new Error(`Data block '${[header]}' not found.`);
             const densityServerCif = CIF.schema.densityServer(block);
-            const volume = await volumeFromDensityServerData(densityServerCif).runInContext(ctx);
+            const volume = await volumeFromDensityServerData(densityServerCif, { entryId: params.entryId }).runInContext(ctx);
             const [x, y, z] = volume.grid.cells.space.dimensions;
             const props = { label: densityServerCif.volume_data_3d_info.name.value(0), description: `Volume ${x}\u00D7${y}\u00D7${z}` };
             return new SO.Volume.Data(volume, props);