Explorar o código

Basic cube format support
- TODO: non-orthogonal frames

David Sehnal %!s(int64=5) %!d(string=hai) anos
pai
achega
99d7a90863

+ 11 - 2
src/mol-io/reader/common/text/tokenizer.ts

@@ -107,13 +107,22 @@ namespace Tokenizer {
         return read;
     }
 
-    /** Advance the state by the given number of lines and return line starts/ends as tokens. */
-    export function readLines(state: Tokenizer, count: number): Tokens {
+    /** Advance the state by the given number of lines and return them*/
+    export function markLines(state: Tokenizer, count: number): Tokens {
         const lineTokens = TokenBuilder.create(state.data, count * 2);
         readLinesChunk(state, count, lineTokens);
         return lineTokens;
     }
 
+    /** Advance the state by the given number of lines and return them */
+    export function readLines(state: Tokenizer, count: number): string[] {
+        const ret: string[] = [];
+        for (let i = 0; i < count; i++) {
+            ret.push(Tokenizer.readLine(state));
+        }
+        return ret;
+    }
+
     /** Advance the state by the given number of lines and return line starts/ends as tokens. */
     export async function readLinesAsync(state: Tokenizer, count: number, ctx: RuntimeContext, initialLineCount = 100000): Promise<Tokens> {
         const { length } = state;

+ 140 - 0
src/mol-io/reader/cube/parser.ts

@@ -0,0 +1,140 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * Adapted from NGL.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Vec3 } from '../../../mol-math/linear-algebra';
+import { Tokenizer } from '../common/text/tokenizer';
+import { Column } from '../../../mol-data/db';
+import { Task, chunkedSubtask, RuntimeContext } from '../../../mol-task';
+import { ReaderResult as Result } from '../result';
+import { parseFloat as fastParseFloat } from '../common/text/number-parser';
+
+// https://h5cube-spec.readthedocs.io/en/latest/cubeformat.html
+
+export interface CubeFile {
+    header: CubeFile.Header,
+    atoms: CubeFile.Atoms,
+    values: Float64Array
+}
+
+export namespace CubeFile {
+    export interface Header {
+        comment1: string,
+        comment2: string,
+        atomCount: number,
+        origin: Vec3,
+        dim: Vec3,
+        basisX: Vec3,
+        basisY: Vec3,
+        basisZ: Vec3,
+        dataSetIds: number[]
+    }
+
+    export interface Atoms {
+        count: number,
+        number: Column<number>,
+        nuclearCharge: Column<number>,
+        x: Column<number>,
+        y: Column<number>,
+        z: Column<number>
+    }
+}
+
+const bohrToAngstromFactor = 0.529177210859;
+
+function readHeader(tokenizer: Tokenizer) {
+    const headerLines = Tokenizer.readLines(tokenizer, 6);
+    const h = (k: number, l: number) => {
+        const field = +headerLines[k].trim().split(/\s+/g)[ l ];
+        return Number.isNaN(field) ? 0 : field;
+    };
+    const basis = (i: number) => {
+        const n = h(i + 2, 0);
+        const s = bohrToAngstromFactor;
+        return [Math.abs(n), Vec3.create(h(i + 2, 1) * s, h(i + 2, 2) * s, h(i + 2, 3) * s), n] as const;
+    };
+
+    const comment1 = headerLines[0].trim();
+    const comment2 = headerLines[1].trim();
+
+    const [atomCount, origin, rawAtomCount] = basis(0);
+    const [NVX, basisX] = basis(1);
+    const [NVY, basisY] = basis(2);
+    const [NVZ, basisZ] = basis(3);
+
+    const atoms = readAtoms(tokenizer, atomCount, bohrToAngstromFactor);
+
+    const dataSetIds: number[] = [];
+    if (rawAtomCount >= 0) {
+        let nVal = h(2, 4);
+        if (nVal === 0) nVal = 1;
+        for (let i = 0; i < nVal; i++) dataSetIds.push(i);
+    } else {
+        const counts = Tokenizer.readLine(tokenizer).trim().split(/\s+/g);
+        for (let i = 0, _i = +counts[0]; i < _i; i++) dataSetIds.push(+counts[i + 1]);
+    }
+
+    const header: CubeFile.Header = { comment1, comment2, atomCount, origin, dim: Vec3.create(NVX, NVY, NVZ), basisX, basisY, basisZ, dataSetIds };
+
+    return { header, atoms };
+}
+
+function readAtoms(tokenizer: Tokenizer, count: number, scaleFactor: number): CubeFile.Atoms {
+    const number = new Int32Array(count);
+    const value = new Float64Array(count);
+    const x = new Float32Array(count);
+    const y = new Float32Array(count);
+    const z = new Float32Array(count);
+
+    for (let i = 0; i < count; i++) {
+        const fields = Tokenizer.readLine(tokenizer).trim().split(/\s+/g);
+        number[i] = +fields[0];
+        value[i] = +fields[1];
+        x[i] = +fields[2] * scaleFactor;
+        y[i] = +fields[3] * scaleFactor;
+        z[i] = +fields[4] * scaleFactor;
+    }
+
+    return {
+        count,
+        number: Column.ofArray({ array: number, schema: Column.Schema.int }),
+        nuclearCharge: Column.ofArray({ array: value, schema: Column.Schema.float }),
+        x: Column.ofArray({ array: x, schema: Column.Schema.float }),
+        y: Column.ofArray({ array: y, schema: Column.Schema.float }),
+        z: Column.ofArray({ array: z, schema: Column.Schema.float })
+    };
+}
+
+function readValues(ctx: RuntimeContext, tokenizer: Tokenizer, header: CubeFile.Header) {
+    const N = header.dim[0] * header.dim[1] * header.dim[2] * header.dataSetIds.length;
+    const chunkSize = 100 * 100 * 100;
+    const data = new Float64Array(N);
+    let offset = 0;
+
+    return chunkedSubtask(ctx, chunkSize, data, (count, data) => {
+        const max = Math.min(N, offset + count);
+        for (let i = offset; i < max; i++) {
+            Tokenizer.skipWhitespace(tokenizer);
+            tokenizer.tokenStart = tokenizer.position;
+            Tokenizer.eatValue(tokenizer);
+            data[i] = fastParseFloat(tokenizer.data, tokenizer.tokenStart, tokenizer.tokenEnd);
+        }
+        offset = max;
+        return max === N ? 0 : chunkSize;
+    }, (ctx, _, i) => ctx.update({ current: Math.min(i, N), max: N }));
+}
+
+export function parseCube(data: string) {
+    return Task.create<Result<CubeFile>>('Parse Cube', async taskCtx => {
+        await taskCtx.update('Reading header...');
+        const tokenizer = Tokenizer(data);
+        const { header, atoms } = readHeader(tokenizer);
+        const values = await readValues(taskCtx, tokenizer, header);
+        return Result.success({ header, atoms, values });
+    });
+}

+ 83 - 0
src/mol-model-formats/structure/cube.ts

@@ -0,0 +1,83 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { Column, Table } from '../../mol-data/db';
+import { Model } from '../../mol-model/structure/model';
+import { MoleculeType, getElementFromAtomicNumber, ElementSymbol } from '../../mol-model/structure/model/types';
+import { RuntimeContext, Task } from '../../mol-task';
+import { createModels } from './basic/parser';
+import { BasicSchema, createBasic } from './basic/schema';
+import { ComponentBuilder } from './common/component';
+import { EntityBuilder } from './common/entity';
+import { ModelFormat } from './format';
+import { CubeFile } from '../../mol-io/reader/cube/parser';
+
+async function getModels(cube: CubeFile, ctx: RuntimeContext): Promise<Model[]> {
+    const { atoms } = cube;
+
+    const MOL = Column.ofConst('MOL', cube.atoms.count, Column.Schema.str);
+    const A = Column.ofConst('A', cube.atoms.count, Column.Schema.str);
+    const type_symbol = Column.ofArray({ array: Column.mapToArray(atoms.number, n => getElementFromAtomicNumber(n)), schema: Column.Schema.Aliased<ElementSymbol>(Column.Schema.str) });
+    const seq_id = Column.ofConst(1, atoms.count, Column.Schema.int);
+
+    const atom_site = Table.ofPartialColumns(BasicSchema.atom_site, {
+        auth_asym_id: A,
+        auth_atom_id: type_symbol,
+        auth_comp_id: MOL,
+        auth_seq_id: seq_id,
+        Cartn_x: Column.asArrayColumn(atoms.x, Float32Array),
+        Cartn_y: Column.asArrayColumn(atoms.y, Float32Array),
+        Cartn_z: Column.asArrayColumn(atoms.z, Float32Array),
+        id: Column.range(0, atoms.count - 1),
+
+        label_asym_id: A,
+        label_atom_id: type_symbol,
+        label_comp_id: MOL,
+        label_seq_id: seq_id,
+        label_entity_id: Column.ofConst('1', atoms.count, Column.Schema.str),
+
+        occupancy: Column.ofConst(1, atoms.count, Column.Schema.float),
+        type_symbol,
+
+        pdbx_PDB_model_num: Column.ofConst(1, atoms.count, Column.Schema.int),
+    }, atoms.count);
+
+    const entityBuilder = new EntityBuilder();
+    entityBuilder.setNames([['MOL', 'Unknown Entity']]);
+    entityBuilder.getEntityId('MOL', MoleculeType.Unknown, 'A');
+
+    const componentBuilder = new ComponentBuilder(seq_id, type_symbol);
+    componentBuilder.setNames([['MOL', 'Unknown Molecule']]);
+    componentBuilder.add('MOL', 0);
+
+    const basics = createBasic({
+        entity: entityBuilder.getEntityTable(),
+        chem_comp: componentBuilder.getChemCompTable(),
+        atom_site
+    });
+
+    return await createModels(basics, MolFormat.create(cube), ctx);
+}
+
+//
+
+export { CubeFormat };
+
+type CubeFormat = ModelFormat<CubeFile>
+
+namespace MolFormat {
+    export function is(x: ModelFormat): x is CubeFormat {
+        return x.kind === 'cube';
+    }
+
+    export function create(cube: CubeFile): CubeFormat {
+        return { kind: 'cube', name: cube.header.comment1, data: cube };
+    }
+}
+
+export function trajectoryFromCube(cube: CubeFile): Task<Model.Trajectory> {
+    return Task.create('Parse Cube', ctx => getModels(cube, ctx));
+}

+ 69 - 0
src/mol-model-formats/volume/cube.ts

@@ -0,0 +1,69 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { VolumeData } from '../../mol-model/volume/data';
+import { Task } from '../../mol-task';
+import { SpacegroupCell, Box3D } from '../../mol-math/geometry';
+import { Tensor, Vec3 } from '../../mol-math/linear-algebra';
+import { arrayMin, arrayMax, arrayMean, arrayRms } from '../../mol-util/array';
+import { CubeFile } from '../../mol-io/reader/cube/parser';
+
+export function volumeFromCube(source: CubeFile, params?: { dataIndex?: number }): Task<VolumeData> {
+    return Task.create<VolumeData>('Create Volume Data', async () => {
+        // TODO: support non-orthogonal axes
+
+        const { header, values: sourceValues } = source;
+        const angles = SpacegroupCell.Zero.anglesInRadians;
+        const size = Vec3.create(header.basisX[0] * header.dim[0], header.basisY[1] * header.dim[1], header.basisZ[2] * header.dim[2]);
+        const cell = SpacegroupCell.create('P 1', size, angles);
+
+        if (header.basisX[1] !== 0 || header.basisX[2] !== 0
+            || header.basisY[0] !== 0 || header.basisY[2] !== 0
+            || header.basisZ[0] !== 0 || header.basisZ[1] !== 0) {
+            throw new Error('Non-orthogonal bases not supported. (TODO)');
+        }
+
+        const origin_frac = Vec3.div(Vec3(), header.origin, size);
+        const dimensions_frac = Vec3.add(Vec3(), origin_frac, Vec3.create(1, 1, 1));
+
+        const space = Tensor.Space(header.dim, [0, 1, 2], Float64Array);
+
+        let values: Float64Array;
+        if (header.dataSetIds.length === 0) {
+            values = sourceValues;
+        } else {
+            // get every nth value from the source values
+            const [h, k, l] = header.dim;
+            const nth = (params?.dataIndex || 0) + 1;
+
+            let o = 0, s = 0;
+
+            values = new Float64Array(h * k * l);
+            for (let u = 0; u < h; u++) {
+                for (let v = 0; v < k; v++) {
+                    for (let w = 0; w < l; w++) {
+                        values[o++] = sourceValues[s];
+                        s += nth;
+                    }
+                }
+            }
+        }
+
+        const data = Tensor.create(space, Tensor.Data1(values));
+
+        return {
+            cell,
+            fractionalBox: Box3D.create(origin_frac, dimensions_frac),
+            data,
+            dataStats: {
+                min: arrayMin(values),
+                max: arrayMax(values),
+                mean: arrayMean(values),
+                sigma: arrayRms(values)
+            }
+        };
+    });
+}

+ 41 - 32
src/mol-model/structure/model/types.ts

@@ -34,6 +34,15 @@ export function ElementSymbol(s: string): ElementSymbol {
     return _esCache[s] || s.toUpperCase();
 }
 
+const _elementByAtomicNumber = new Map(
+    ([[1, 'H'], [2, 'He'], [3, 'Li'], [4, 'Be'], [5, 'B'], [6, 'C'], [7, 'N'], [8, 'O'], [9, 'F'], [10, 'Ne'], [11, 'Na'], [12, 'Mg'], [13, 'Al'], [14, 'Si'], [15, 'P'], [16, 'S'], [17, 'Cl'], [18, 'Ar'], [19, 'K'], [20, 'Ca'], [21, 'Sc'], [22, 'Ti'], [23, 'V'], [24, 'Cr'], [25, 'Mn'], [26, 'Fe'], [27, 'Co'], [28, 'Ni'], [29, 'Cu'], [30, 'Zn'], [31, 'Ga'], [32, 'Ge'], [33, 'As'], [34, 'Se'], [35, 'Br'], [36, 'Kr'], [37, 'Rb'], [38, 'Sr'], [39, 'Y'], [40, 'Zr'], [41, 'Nb'], [42, 'Mo'], [43, 'Tc'], [44, 'Ru'], [45, 'Rh'], [46, 'Pd'], [47, 'Ag'], [48, 'Cd'], [49, 'In'], [50, 'Sn'], [51, 'Sb'], [52, 'Te'], [53, 'I'], [54, 'Xe'], [55, 'Cs'], [56, 'Ba'], [57, 'La'], [58, 'Ce'], [59, 'Pr'], [60, 'Nd'], [61, 'Pm'], [62, 'Sm'], [63, 'Eu'], [64, 'Gd'], [65, 'Tb'], [66, 'Dy'], [67, 'Ho'], [68, 'Er'], [69, 'Tm'], [70, 'Yb'], [71, 'Lu'], [72, 'Hf'], [73, 'Ta'], [74, 'W'], [75, 'Re'], [76, 'Os'], [77, 'Ir'], [78, 'Pt'], [79, 'Au'], [80, 'Hg'], [81, 'Tl'], [82, 'Pb'], [83, 'Bi'], [84, 'Po'], [85, 'At'], [86, 'Rn'], [87, 'Fr'], [88, 'Ra'], [89, 'Ac'], [90, 'Th'], [91, 'Pa'], [92, 'U'], [93, 'Np'], [94, 'Pu'], [95, 'Am'], [96, 'Cm'], [97, 'Bk'], [98, 'Cf'], [99, 'Es'], [100, 'Fm'], [101, 'Md'], [102, 'No'], [103, 'Lr'], [104, 'Rf'], [105, 'Db'], [106, 'Sg'], [107, 'Bh'], [108, 'Hs'], [109, 'Mt'], [110, 'Ds'], [111, 'Rg'], [112, 'Cn'], [113, 'Uut'], [114, 'Fl'], [115, 'Uup'], [116, 'Lv'], [117, 'Uus'], [118, 'Uuo']] as const)
+        .map(e => [e[0], ElementSymbol(e[1])]));
+
+export function getElementFromAtomicNumber(n: number) {
+    if (_elementByAtomicNumber.has(n as any)) return _elementByAtomicNumber.get(n as any)!;
+    return ElementSymbol('H');
+}
+
 /** Entity types as defined in the mmCIF dictionary */
 export const enum EntityType {
     'unknown', 'polymer', 'non-polymer', 'macrolide', 'water', 'branched'
@@ -256,9 +265,9 @@ export const DnaBaseNames = new Set([
     'DA', 'DC', 'DT', 'DG', 'DI', 'DU',
     'DN' // unknown DNA base from CCD
 ]);
-export const PeptideBaseNames = new Set([ 'APN', 'CPN', 'TPN', 'GPN' ]);
-export const PurineBaseNames = new Set([ 'A', 'G', 'I', 'DA', 'DG', 'DI', 'APN', 'GPN' ]);
-export const PyrimidineBaseNames = new Set([ 'C', 'T', 'U', 'DC', 'DT', 'DU', 'CPN', 'TPN' ]);
+export const PeptideBaseNames = new Set(['APN', 'CPN', 'TPN', 'GPN']);
+export const PurineBaseNames = new Set(['A', 'G', 'I', 'DA', 'DG', 'DI', 'APN', 'GPN']);
+export const PyrimidineBaseNames = new Set(['C', 'T', 'U', 'DC', 'DT', 'DU', 'CPN', 'TPN']);
 export const BaseNames = SetUtils.unionMany(RnaBaseNames, DnaBaseNames, PeptideBaseNames);
 
 export const isPurineBase = (compId: string) => PurineBaseNames.has(compId.toUpperCase());
@@ -596,13 +605,13 @@ export type BondType = BitFlags<BondType.Flag>
 export namespace BondType {
     export const is: (b: BondType, f: Flag) => boolean = BitFlags.has;
     export const enum Flag {
-        None                 = 0x0,
-        Covalent             = 0x1,
+        None = 0x0,
+        Covalent = 0x1,
         MetallicCoordination = 0x2,
-        HydrogenBond         = 0x4,
-        Disulfide            = 0x8,
-        Aromatic             = 0x10,
-        Computed             = 0x20
+        HydrogenBond = 0x4,
+        Disulfide = 0x8,
+        Aromatic = 0x10,
+        Computed = 0x20
         // currently at most 16 flags are supported!!
     }
 
@@ -656,28 +665,28 @@ export namespace BondType {
  */
 export const ResidueHydrophobicity = {
     // AA  DGwif   DGwoct  Oct-IF
-    'ALA': [ 0.17, 0.50, 0.33 ],
-    'ARG': [ 0.81, 1.81, 1.00 ],
-    'ASN': [ 0.42, 0.85, 0.43 ],
-    'ASP': [ 1.23, 3.64, 2.41 ],
-    'ASH': [ -0.07, 0.43, 0.50 ],
-    'CYS': [ -0.24, -0.02, 0.22 ],
-    'GLN': [ 0.58, 0.77, 0.19 ],
-    'GLU': [ 2.02, 3.63, 1.61 ],
-    'GLH': [ -0.01, 0.11, 0.12 ],
-    'GLY': [ 0.01, 1.15, 1.14 ],
+    'ALA': [0.17, 0.50, 0.33],
+    'ARG': [0.81, 1.81, 1.00],
+    'ASN': [0.42, 0.85, 0.43],
+    'ASP': [1.23, 3.64, 2.41],
+    'ASH': [-0.07, 0.43, 0.50],
+    'CYS': [-0.24, -0.02, 0.22],
+    'GLN': [0.58, 0.77, 0.19],
+    'GLU': [2.02, 3.63, 1.61],
+    'GLH': [-0.01, 0.11, 0.12],
+    'GLY': [0.01, 1.15, 1.14],
     // "His+": [  0.96,  2.33,  1.37 ],
-    'HIS': [ 0.17, 0.11, -0.06 ],
-    'ILE': [ -0.31, -1.12, -0.81 ],
-    'LEU': [ -0.56, -1.25, -0.69 ],
-    'LYS': [ 0.99, 2.80, 1.81 ],
-    'MET': [ -0.23, -0.67, -0.44 ],
-    'PHE': [ -1.13, -1.71, -0.58 ],
-    'PRO': [ 0.45, 0.14, -0.31 ],
-    'SER': [ 0.13, 0.46, 0.33 ],
-    'THR': [ 0.14, 0.25, 0.11 ],
-    'TRP': [ -1.85, -2.09, -0.24 ],
-    'TYR': [ -0.94, -0.71, 0.23 ],
-    'VAL': [ 0.07, -0.46, -0.53 ]
+    'HIS': [0.17, 0.11, -0.06],
+    'ILE': [-0.31, -1.12, -0.81],
+    'LEU': [-0.56, -1.25, -0.69],
+    'LYS': [0.99, 2.80, 1.81],
+    'MET': [-0.23, -0.67, -0.44],
+    'PHE': [-1.13, -1.71, -0.58],
+    'PRO': [0.45, 0.14, -0.31],
+    'SER': [0.13, 0.46, 0.33],
+    'THR': [0.14, 0.25, 0.11],
+    'TRP': [-1.85, -2.09, -0.24],
+    'TYR': [-0.94, -0.71, 0.23],
+    'VAL': [0.07, -0.46, -0.53]
 };
-export const DefaultResidueHydrophobicity = [ 0.00, 0.00, 0.00 ];
+export const DefaultResidueHydrophobicity = [0.00, 0.00, 0.00];

+ 53 - 0
src/mol-plugin-state/formats/volume.ts

@@ -13,6 +13,8 @@ import { PluginStateObject } from '../objects';
 import { VolumeRepresentation3DHelpers } from '../transforms/representation';
 import { ColorNames } from '../../mol-util/color/names';
 import { VolumeIsoValue } from '../../mol-model/volume';
+import { createVolumeRepresentationParams } from '../helpers/volume-representation-params';
+import { objectForEach } from '../../mol-util/object';
 
 const Category = 'Volume';
 
@@ -59,6 +61,56 @@ export const Dsn6Provider = DataFormatProvider({
     visuals: defaultVisuals
 });
 
+export const CubeProvider = DataFormatProvider({
+    label: 'Cube',
+    description: 'Cube',
+    category: Category,
+    stringExtensions: ['cub', 'cube'],
+    parse: async (plugin, data) => {
+        const format = plugin.build()
+            .to(data)
+            .apply(StateTransforms.Data.ParseCube, {}, { state: { isGhost: true } });
+
+        const volume = format.apply(StateTransforms.Volume.VolumeFromCube);
+        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 });
+
+        return { format: format.selector, volume: volume.selector, structure: structure.selector };
+    },
+    visuals: async (plugin: PluginContext, data: { volume: StateObjectSelector<PluginStateObject.Volume.Data>, structure: StateObjectSelector<PluginStateObject.Molecule.Structure> }) => {
+        const surfaces = plugin.build();
+
+        const volumeData = data.volume.cell?.obj?.data;
+        const volumePos = surfaces.to(data.volume).apply(StateTransforms.Representation.VolumeRepresentation3D, createVolumeRepresentationParams(plugin, volumeData, {
+            type: 'isosurface',
+            typeParams: { isoValue: VolumeIsoValue.relative(1), alpha: 0.4 },
+            color: 'uniform',
+            colorParams: { value: ColorNames.red }
+        }));
+        const volumeNeg = surfaces.to(data.volume).apply(StateTransforms.Representation.VolumeRepresentation3D, createVolumeRepresentationParams(plugin, volumeData, {
+            type: 'isosurface',
+            typeParams: { isoValue: VolumeIsoValue.relative(-1), alpha: 0.4 },
+            color: 'uniform',
+            colorParams: { value: ColorNames.blue }
+        }));
+
+        const structure = await plugin.builders.structure.representation.applyPreset(data.structure, 'auto');
+        await surfaces.commit();
+
+        const structureReprs: StateObjectSelector<PluginStateObject.Molecule.Structure.Representation3D>[] = [];
+
+        objectForEach(structure?.representations as any, (r: any) => {
+            if (r) structureReprs.push(r);
+        });
+
+        return [volumePos.selector, volumeNeg.selector, ...structureReprs];
+    }
+});
+
 export const DscifProvider = DataFormatProvider({
     label: 'DensityServer CIF',
     description: 'DensityServer CIF',
@@ -112,6 +164,7 @@ export const DscifProvider = DataFormatProvider({
 export const BuiltInVolumeFormats = [
     ['ccp4', Ccp4Provider] as const,
     ['dns6', Dsn6Provider] as const,
+    ['cube', CubeProvider] as const,
     ['dscif', DscifProvider] as const,
 ] as const;
 

+ 121 - 0
src/mol-plugin-state/helpers/volume-representation-params.ts

@@ -0,0 +1,121 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { VolumeData } from '../../mol-model/volume';
+import { PluginContext } from '../../mol-plugin/context';
+import { RepresentationProvider } from '../../mol-repr/representation';
+import { VolumeRepresentationRegistry } from '../../mol-repr/volume/registry';
+import { StateTransformer } from '../../mol-state';
+import { ColorTheme } from '../../mol-theme/color';
+import { SizeTheme } from '../../mol-theme/size';
+import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { VolumeRepresentation3D } from '../transforms/representation';
+
+export interface VolumeRepresentationBuiltInProps<
+    R extends VolumeRepresentationRegistry.BuiltIn = VolumeRepresentationRegistry.BuiltIn,
+    C extends ColorTheme.BuiltIn = ColorTheme.BuiltIn,
+    S extends SizeTheme.BuiltIn = SizeTheme.BuiltIn> {
+    /** Using any registered name will work, but code completion will break */
+    type?: R,
+    typeParams?: VolumeRepresentationRegistry.BuiltInParams<R>,
+    /** Using any registered name will work, but code completion will break */
+    color?: C,
+    colorParams?: ColorTheme.BuiltInParams<C>,
+    /** Using any registered name will work, but code completion will break */
+    size?: S,
+    sizeParams?: SizeTheme.BuiltInParams<S>
+}
+
+export interface VolumeRepresentationProps<
+    R extends RepresentationProvider<VolumeData> = RepresentationProvider<VolumeData>,
+    C extends ColorTheme.Provider = ColorTheme.Provider,
+    S extends SizeTheme.Provider = SizeTheme.Provider> {
+    type?: R,
+    typeParams?: Partial<RepresentationProvider.ParamValues<R>>,
+    color?: C,
+    colorParams?: Partial<ColorTheme.ParamValues<C>>,
+    size?: S,
+    sizeParams?: Partial<SizeTheme.ParamValues<S>>
+}
+
+export function createVolumeRepresentationParams<R extends VolumeRepresentationRegistry.BuiltIn, C extends ColorTheme.BuiltIn, S extends SizeTheme.BuiltIn>(ctx: PluginContext, volume?: VolumeData, props?: VolumeRepresentationBuiltInProps<R, C, S>): StateTransformer.Params<VolumeRepresentation3D>
+export function createVolumeRepresentationParams<R extends RepresentationProvider<VolumeData>, C extends ColorTheme.Provider, S extends SizeTheme.Provider>(ctx: PluginContext, volume?: VolumeData, props?: VolumeRepresentationProps<R, C, S>): StateTransformer.Params<VolumeRepresentation3D>
+export function createVolumeRepresentationParams(ctx: PluginContext, volume?: VolumeData, props: any = {}): StateTransformer.Params<VolumeRepresentation3D>  {
+    const p = props as VolumeRepresentationBuiltInProps;
+    if (typeof p.type === 'string' || typeof p.color === 'string' || typeof p.size === 'string') return createParamsByName(ctx, volume || VolumeData.One, props);
+    return createParamsProvider(ctx, volume || VolumeData.One, props);
+}
+
+export function getVolumeThemeTypes(ctx: PluginContext, volume?: VolumeData) {
+    const { themes: themeCtx } = ctx.representation.volume;
+    if (!volume) return themeCtx.colorThemeRegistry.types;
+    return themeCtx.colorThemeRegistry.getApplicableTypes({ volume });
+}
+
+export function createVolumeColorThemeParams<T extends ColorTheme.BuiltIn>(ctx: PluginContext, volume: VolumeData | undefined, typeName: string | undefined, themeName: T, params?: ColorTheme.BuiltInParams<T>): StateTransformer.Params<VolumeRepresentation3D>['colorTheme']
+export function createVolumeColorThemeParams(ctx: PluginContext, volume: VolumeData | undefined, typeName: string | undefined, themeName?: string, params?: any): StateTransformer.Params<VolumeRepresentation3D>['colorTheme']
+export function createVolumeColorThemeParams(ctx: PluginContext, volume: VolumeData | undefined, typeName: string | undefined, themeName?: string, params?: any): StateTransformer.Params<VolumeRepresentation3D>['colorTheme'] {
+    const { registry, themes } = ctx.representation.volume;
+    const repr = registry.get(typeName || registry.default.name);
+    const color = themes.colorThemeRegistry.get(themeName || repr.defaultColorTheme.name);
+    const colorDefaultParams = PD.getDefaultValues(color.getParams({ volume: volume || VolumeData.One }));
+    if (color.name === repr.defaultColorTheme.name) Object.assign(colorDefaultParams, repr.defaultColorTheme.props);
+    return { name: color.name, params: Object.assign(colorDefaultParams, params) };
+}
+
+export function createVolumeSizeThemeParams<T extends SizeTheme.BuiltIn>(ctx: PluginContext, volume: VolumeData | undefined, typeName: string | undefined, themeName: T, params?: SizeTheme.BuiltInParams<T>): StateTransformer.Params<VolumeRepresentation3D>['sizeTheme']
+export function createVolumeSizeThemeParams(ctx: PluginContext, volume: VolumeData | undefined, typeName: string | undefined, themeName?: string, params?: any): StateTransformer.Params<VolumeRepresentation3D>['sizeTheme']
+export function createVolumeSizeThemeParams(ctx: PluginContext, volume: VolumeData | undefined, typeName: string | undefined, themeName?: string, params?: any): StateTransformer.Params<VolumeRepresentation3D>['sizeTheme'] {
+    const { registry, themes } = ctx.representation.volume;
+    const repr = registry.get(typeName || registry.default.name);
+    const size = themes.sizeThemeRegistry.get(themeName || repr.defaultSizeTheme.name);
+    const sizeDefaultParams = PD.getDefaultValues(size.getParams({ volume: volume || VolumeData.One }));
+    if (size.name === repr.defaultSizeTheme.name) Object.assign(sizeDefaultParams, repr.defaultSizeTheme.props);
+    return { name: size.name, params: Object.assign(sizeDefaultParams, params) };
+}
+
+function createParamsByName(ctx: PluginContext, volume: VolumeData, props: VolumeRepresentationBuiltInProps): StateTransformer.Params<VolumeRepresentation3D> {
+    const typeProvider = (props.type && ctx.representation.volume.registry.get(props.type))
+        || ctx.representation.volume.registry.default.provider;
+    const colorProvider = (props.color && ctx.representation.volume.themes.colorThemeRegistry.get(props.color))
+        || ctx.representation.volume.themes.colorThemeRegistry.get(typeProvider.defaultColorTheme.name);
+    const sizeProvider = (props.size && ctx.representation.volume.themes.sizeThemeRegistry.get(props.size))
+        || ctx.representation.volume.themes.sizeThemeRegistry.get(typeProvider.defaultSizeTheme.name);
+
+    return createParamsProvider(ctx, volume, {
+        type: typeProvider,
+        typeParams: props.typeParams,
+        color: colorProvider,
+        colorParams: props.colorParams,
+        size: sizeProvider,
+        sizeParams: props.sizeParams
+    });
+}
+
+function createParamsProvider(ctx: PluginContext, volume: VolumeData, props: VolumeRepresentationProps = {}): StateTransformer.Params<VolumeRepresentation3D> {
+    const { themes: themeCtx } = ctx.representation.volume;
+    const themeDataCtx = { volume };
+
+    const repr = props.type || ctx.representation.volume.registry.default.provider;
+    const reprDefaultParams = PD.getDefaultValues(repr.getParams(themeCtx, volume));
+    const reprParams = Object.assign(reprDefaultParams, props.typeParams);
+
+    const color = props.color || themeCtx.colorThemeRegistry.get(repr.defaultColorTheme.name);
+    const colorDefaultParams = PD.getDefaultValues(color.getParams(themeDataCtx));
+    if (color.name === repr.defaultColorTheme.name) Object.assign(colorDefaultParams, repr.defaultColorTheme.props);
+    const colorParams = Object.assign(colorDefaultParams, props.colorParams);
+
+    const size = props.size || themeCtx.sizeThemeRegistry.get(repr.defaultSizeTheme.name);
+    const sizeDefaultParams = PD.getDefaultValues(size.getParams(themeDataCtx));
+    if (size.name === repr.defaultSizeTheme.name) Object.assign(sizeDefaultParams, repr.defaultSizeTheme.props);
+    const sizeParams = Object.assign(sizeDefaultParams, props.sizeParams);
+
+    return ({
+        type: { name: repr.name, params: reprParams },
+        colorTheme: { name: color.name, params: colorParams },
+        sizeTheme: { name: size.name, params: sizeParams }
+    });
+}

+ 2 - 0
src/mol-plugin-state/objects.ts

@@ -21,6 +21,7 @@ import { ShapeRepresentation } from '../mol-repr/shape/representation';
 import { StructureRepresentation, StructureRepresentationState } from '../mol-repr/structure/representation';
 import { VolumeRepresentation } from '../mol-repr/volume/representation';
 import { StateObject, StateTransformer } from '../mol-state';
+import { CubeFile } from '../mol-io/reader/cube/parser';
 
 export type TypeClass = 'root' | 'data' | 'prop'
 
@@ -67,6 +68,7 @@ export namespace PluginStateObject {
     export namespace Format {
         export class Json extends Create<any>({ name: 'JSON Data', typeClass: 'Data' }) { }
         export class Cif extends Create<CifFile>({ name: 'CIF File', typeClass: 'Data' }) { }
+        export class Cube extends Create<CubeFile>({ name: 'Cube File', typeClass: 'Data' }) { }
         export class Psf extends Create<PsfFile>({ name: 'PSF File', typeClass: 'Data' }) { }
         export class Ply extends Create<PlyFile>({ name: 'PLY File', typeClass: 'Data' }) { }
         export class Ccp4 extends Create<Ccp4File>({ name: 'CCP4/MRC/MAP File', typeClass: 'Data' }) { }

+ 18 - 0
src/mol-plugin-state/transforms/data.ts

@@ -18,6 +18,7 @@ import { ajaxGetMany } from '../../mol-util/data-source';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { PluginStateObject as SO, PluginStateTransform } from '../objects';
 import { Asset } from '../../mol-util/assets';
+import { parseCube } from '../../mol-io/reader/cube/parser';
 
 export { Download };
 export { DownloadBlob };
@@ -25,6 +26,7 @@ export { RawData };
 export { ReadFile };
 export { ParseBlob };
 export { ParseCif };
+export { ParseCube };
 export { ParsePsf };
 export { ParsePly };
 export { ParseCcp4 };
@@ -255,6 +257,22 @@ const ParseCif = PluginStateTransform.BuiltIn({
     }
 });
 
+type ParseCube = typeof ParseCube
+const ParseCube = PluginStateTransform.BuiltIn({
+    name: 'parse-cube',
+    display: { name: 'Parse Cube', description: 'Parse Cube from String data' },
+    from: SO.Data.String,
+    to: SO.Format.Cube
+})({
+    apply({ a }) {
+        return Task.create('Parse Cube', async ctx => {
+            const parsed = await parseCube(a.data).runInContext(ctx);
+            if (parsed.isError) throw new Error(parsed.message);
+            return new SO.Format.Cube(parsed.result);
+        });
+    }
+});
+
 type ParsePsf = typeof ParsePsf
 const ParsePsf = PluginStateTransform.BuiltIn({
     name: 'parse-psf',

+ 18 - 0
src/mol-plugin-state/transforms/model.ts

@@ -34,6 +34,7 @@ import { PluginStateObject as SO, PluginStateTransform } from '../objects';
 import { parseMol } from '../../mol-io/reader/mol/parser';
 import { trajectoryFromMol } from '../../mol-model-formats/structure/mol';
 import { trajectoryFromCifCore } from '../../mol-model-formats/structure/cif-core';
+import { trajectoryFromCube } from '../../mol-model-formats/structure/cube';
 
 export { CoordinatesFromDcd };
 export { TopologyFromPsf };
@@ -43,6 +44,7 @@ export { TrajectoryFromMmCif };
 export { TrajectoryFromPDB };
 export { TrajectoryFromGRO };
 export { TrajectoryFromMOL };
+export { TrajectoryFromCube };
 export { TrajectoryFromCifCore };
 export { TrajectoryFrom3DG };
 export { ModelFromTrajectory };
@@ -233,6 +235,22 @@ const TrajectoryFromMOL = PluginStateTransform.BuiltIn({
     }
 });
 
+type TrajectoryFromCube = typeof TrajectoryFromCube
+const TrajectoryFromCube = PluginStateTransform.BuiltIn({
+    name: 'trajectory-from-cube',
+    display: { name: 'Parse Cube', description: 'Parse Cube file to create a trajectory.' },
+    from: SO.Format.Cube,
+    to: SO.Molecule.Trajectory
+})({
+    apply({ a }) {
+        return Task.create('Parse MOL', async ctx => {
+            const models = await trajectoryFromCube(a.data).runInContext(ctx);
+            const props = { label: `${models[0].entry}`, description: `${models.length} model${models.length === 1 ? '' : 's'}` };
+            return new SO.Molecule.Trajectory(models, props);
+        });
+    }
+});
+
 type TrajectoryFromCifCore = typeof TrajectoryFromCifCore
 const TrajectoryFromCifCore = PluginStateTransform.BuiltIn({
     name: 'trajectory-from-cif-core',

+ 25 - 0
src/mol-plugin-state/transforms/volume.ts

@@ -13,10 +13,13 @@ import { volumeFromDsn6 } from '../../mol-model-formats/volume/dsn6';
 import { Task } from '../../mol-task';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { PluginStateObject as SO, PluginStateTransform } from '../objects';
+import { volumeFromCube } from '../../mol-model-formats/volume/cube';
 
 export { VolumeFromCcp4 };
 export { VolumeFromDsn6 };
+export { VolumeFromCube };
 export { VolumeFromDensityServerCif };
+
 type VolumeFromCcp4 = typeof VolumeFromCcp4
 const VolumeFromCcp4 = PluginStateTransform.BuiltIn({
     name: 'volume-from-ccp4',
@@ -60,6 +63,28 @@ const VolumeFromDsn6 = PluginStateTransform.BuiltIn({
     }
 });
 
+type VolumeFromCube = typeof VolumeFromCube
+const VolumeFromCube = PluginStateTransform.BuiltIn({
+    name: 'volume-from-cube',
+    display: { name: 'Volume from Cube', description: 'Create Volume from Cube data' },
+    from: SO.Format.Cube,
+    to: SO.Volume.Data,
+    params(a) {
+        if (!a) return { dataIndex: PD.Numeric(0) };
+        return {
+            dataIndex: PD.Select(0, a.data.header.dataSetIds.map((id, i) => [i, `${id}`] as const))
+        };
+    }
+})({
+    apply({ a, params }) {
+        return Task.create('Create volume from Cube', async ctx => {
+            const volume = await volumeFromCube(a.data, params).runInContext(ctx);
+            const props = { label: 'Volume' };
+            return new SO.Volume.Data(volume, props);
+        });
+    }
+});
+
 type VolumeFromDensityServerCif = typeof VolumeFromDensityServerCif
 const VolumeFromDensityServerCif = PluginStateTransform.BuiltIn({
     name: 'volume-from-density-server-cif',

+ 2 - 0
src/mol-plugin/index.ts

@@ -59,6 +59,8 @@ export const DefaultPluginSpec: PluginSpec = {
         PluginSpec.Action(StateTransforms.Representation.TransparencyStructureRepresentation3DFromScript),
 
         PluginSpec.Action(StateTransforms.Volume.VolumeFromCcp4),
+        PluginSpec.Action(StateTransforms.Volume.VolumeFromDsn6),
+        PluginSpec.Action(StateTransforms.Volume.VolumeFromCube),
         PluginSpec.Action(StateTransforms.Representation.VolumeRepresentation3D),
     ],
     behaviors: [

+ 1 - 0
src/mol-util/object.ts

@@ -106,6 +106,7 @@ export function mapObjectMap<T, S>(o: { [k: string]: T }, f: (v: T) => S): { [k:
 }
 
 export function objectForEach<T>(o: { [k: string]: T }, f: (v: T, k: string) => void) {
+    if (!o) return;
     for (const k of Object.keys(o)) {
         f((o as any)[k], k);
     }

+ 1 - 1
tsconfig.servers.json

@@ -19,5 +19,5 @@
         "rootDir": "src",
         "outDir": "lib/servers"
     },
-    "include": [ "src/servers/**/*" ]
+    "include": [ "src/servers/**/*", "src/perf-tests/*" ]
 }