Bladeren bron

first class Trajectory object
- baseline for interpolation and async fetching support

David Sehnal 4 jaren geleden
bovenliggende
commit
482059cc9b

+ 9 - 7
src/cli/structure-info/model.ts

@@ -10,7 +10,7 @@ import * as argparse from 'argparse';
 require('util.promisify').shim();
 
 import { CifFrame } from '../../mol-io/reader/cif';
-import { Model, Structure, StructureElement, Unit, StructureProperties, UnitRing } from '../../mol-model/structure';
+import { Model, Structure, StructureElement, Unit, StructureProperties, UnitRing, Trajectory } from '../../mol-model/structure';
 // import { Run, Progress } from '../../mol-task'
 import { OrderedSet } from '../../mol-data/int';
 import { openCif, downloadCif } from './helpers';
@@ -19,6 +19,7 @@ import { trajectoryFromMmCIF } from '../../mol-model-formats/structure/mmcif';
 import { Sequence } from '../../mol-model/sequence';
 import { ModelSecondaryStructure } from '../../mol-model-formats/structure/property/secondary-structure';
 import { ModelSymmetry } from '../../mol-model-formats/structure/property/symmetry';
+import { Task } from '../../mol-task';
 
 
 async function downloadFromPdb(pdb: string) {
@@ -183,10 +184,11 @@ export function printSymmetryInfo(model: Model) {
     console.log(`NCS operators: ${symmetry.ncsOperators && symmetry.ncsOperators.map(a => a.name).join(', ')}`);
 }
 
-export function printModelStats(models: ReadonlyArray<Model>) {
+export async function printModelStats(models: Trajectory) {
     console.log('\nModels\n=============');
 
-    for (const m of models) {
+    for (let i = 0; i < models.frameCount; i++) {
+        const m = await Task.resolveInContext(models.getFrameAtIndex(i));
         if (m.coarseHierarchy.isDefined) {
             console.log(`${m.label} ${m.modelNum}: ${m.atomicHierarchy.atoms._rowCount} atom(s), ${m.coarseHierarchy.spheres.count} sphere(s), ${m.coarseHierarchy.gaussians.count} gaussian(s)`);
         } else {
@@ -198,7 +200,7 @@ export function printModelStats(models: ReadonlyArray<Model>) {
 
 export async function getModelsAndStructure(frame: CifFrame) {
     const models = await trajectoryFromMmCIF(frame).run();
-    const structure = Structure.ofModel(models[0]);
+    const structure = Structure.ofModel(models.representative);
     return { models, structure };
 }
 
@@ -206,13 +208,13 @@ async function run(frame: CifFrame, args: Args) {
     const { models, structure } = await getModelsAndStructure(frame);
 
     if (args.models) printModelStats(models);
-    if (args.seq) printSequence(models[0]);
+    if (args.seq) printSequence(models.representative);
     if (args.units) printUnits(structure);
-    if (args.sym) printSymmetryInfo(models[0]);
+    if (args.sym) printSymmetryInfo(models.representative);
     if (args.rings) printRings(structure);
     if (args.intraBonds) printBonds(structure, true, false);
     if (args.interBonds) printBonds(structure, false, true);
-    if (args.sec) printSecStructure(models[0]);
+    if (args.sec) printSecStructure(models.representative);
 }
 
 async function runDL(pdb: string, args: Args) {

+ 6 - 6
src/extensions/cellpack/model.ts

@@ -10,7 +10,7 @@ import { PluginStateObject as PSO } from '../../mol-plugin-state/objects';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { Ingredient, IngredientSource, CellPacking } from './data';
 import { getFromPdb, getFromCellPackDB, IngredientFiles, parseCif, parsePDBfile, getStructureMean, getFromOPM } from './util';
-import { Model, Structure, StructureSymmetry, StructureSelection, QueryContext, Unit } from '../../mol-model/structure';
+import { Model, Structure, StructureSymmetry, StructureSelection, QueryContext, Unit, Trajectory } from '../../mol-model/structure';
 import { trajectoryFromMmCIF, MmcifFormat } from '../../mol-model-formats/structure/mmcif';
 import { trajectoryFromPDB } from '../../mol-model-formats/structure/pdb';
 import { Mat4, Vec3, Quat } from '../../mol-math/linear-algebra';
@@ -36,8 +36,8 @@ function getCellPackModelUrl(fileName: string, baseUrl: string) {
 }
 
 class TrajectoryCache {
-    private map = new Map<string, Model.Trajectory>();
-    set(id: string, trajectory: Model.Trajectory) { this.map.set(id, trajectory); }
+    private map = new Map<string, Trajectory>();
+    set(id: string, trajectory: Trajectory) { this.map.set(id, trajectory); }
     get(id: string) { return this.map.get(id); }
 }
 
@@ -94,9 +94,9 @@ async function getModel(plugin: PluginContext, id: string, ingredient: Ingredien
                 trajectory = await plugin.runTask(trajectoryFromMmCIF(data.mmcif));
             }
         }
-        trajCache.set(id, trajectory);
+        trajCache.set(id, trajectory!);
     }
-    const model = trajectory[modelIndex];
+    const model = await plugin.resolveTask(trajectory?.getFrameAtIndex(modelIndex)!);
     return { model, assets };
 }
 
@@ -303,7 +303,7 @@ async function getCurve(plugin: PluginContext, name: string, ingredient: Ingredi
     const curveModelTask = Task.create('Curve Model', async ctx => {
         const format = MmcifFormat.fromFrame(cif);
         const models = await createModels(format.data.db, format, ctx);
-        return models[0];
+        return models.representative;
     });
 
     const curveModel = await plugin.runTask(curveModelTask);

+ 2 - 2
src/extensions/g3d/model.ts

@@ -4,7 +4,6 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { Model } from '../../mol-model/structure/model';
 import { Task } from '../../mol-task';
 import { Column, Table } from '../../mol-data/db';
 import { MoleculeType } from '../../mol-model/structure/model/types';
@@ -13,6 +12,7 @@ import { BasicSchema, createBasic } from '../../mol-model-formats/structure/basi
 import { createModels } from '../../mol-model-formats/structure/basic/parser';
 import { G3dDataBlock } from './data';
 import { objectForEach } from '../../mol-util/object';
+import { Trajectory } from '../../mol-model/structure';
 
 interface Columns {
     entity_id: string[],
@@ -106,7 +106,7 @@ function getBasic(data: G3dDataBlock) {
     });
 }
 
-export function trajectoryFromG3D(data: G3dDataBlock): Task<Model.Trajectory> {
+export function trajectoryFromG3D(data: G3dDataBlock): Task<Trajectory> {
     return Task.create('Parse G3D', async ctx => {
         const basic = getBasic(data);
         return createModels(basic, { kind: 'g3d', name: 'G3D', data }, ctx);

+ 2 - 2
src/mol-model-formats/structure/3dg.ts

@@ -4,7 +4,6 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Model } from '../../mol-model/structure/model';
 import { Task } from '../../mol-task';
 import { ModelFormat } from '../format';
 import { Column, Table } from '../../mol-data/db';
@@ -14,6 +13,7 @@ import { fillSerial } from '../../mol-util/array';
 import { MoleculeType } from '../../mol-model/structure/model/types';
 import { BasicSchema, createBasic } from './basic/schema';
 import { createModels } from './basic/parser';
+import { Trajectory } from '../../mol-model/structure';
 
 function getBasic(table: File3DG['table']) {
     const entityIds = new Array<string>(table._rowCount);
@@ -74,7 +74,7 @@ namespace Format3dg {
     }
 }
 
-export function trajectoryFrom3DG(file3dg: File3DG): Task<Model.Trajectory> {
+export function trajectoryFrom3DG(file3dg: File3DG): Task<Trajectory> {
     return Task.create('Parse 3DG', async ctx => {
         const format = Format3dg.from3dg(file3dg);
         const basic = getBasic(file3dg.table);

+ 2 - 1
src/mol-model-formats/structure/basic/parser.ts

@@ -21,6 +21,7 @@ import { AtomSite, BasicData } from './schema';
 import { getProperties } from './properties';
 import { getEntities } from './entities';
 import { getModelGroupName } from './util';
+import { ArrayTrajectory } from '../../../mol-model/structure/trajectory';
 
 export async function createModels(data: BasicData, format: ModelFormat, ctx: RuntimeContext) {
     const properties = getProperties(data);
@@ -32,7 +33,7 @@ export async function createModels(data: BasicData, format: ModelFormat, ctx: Ru
         Model.TrajectoryInfo.set(models[i], { index: i, size: models.length });
     }
 
-    return models;
+    return new ArrayTrajectory(models);
 }
 
 /** Standard atomic model */

+ 8 - 5
src/mol-model-formats/structure/cif-core.ts

@@ -21,6 +21,7 @@ import { ModelSymmetry } from './property/symmetry';
 import { IndexPairBonds } from './property/bonds/index-pair';
 import { AtomSiteAnisotrop } from './property/anisotropic';
 import { guessElementSymbolString } from './util';
+import { Trajectory } from '../../mol-model/structure';
 
 function getSpacegroupNameOrNumber(space_group: CifCore_Database['space_group']) {
     const groupNumber = space_group.IT_number.value(0);
@@ -45,7 +46,7 @@ function getSymmetry(db: CifCore_Database): Symmetry {
     };
 }
 
-async function getModels(db: CifCore_Database, format: CifCoreFormat, ctx: RuntimeContext): Promise<Model[]> {
+async function getModels(db: CifCore_Database, format: CifCoreFormat, ctx: RuntimeContext) {
 
     const atomCount = db.atom_site._rowCount;
     const MOL = Column.ofConst('MOL', atomCount, Column.Schema.str);
@@ -152,8 +153,10 @@ async function getModels(db: CifCore_Database, format: CifCoreFormat, ctx: Runti
 
     const models = await createModels(basics, format, ctx);
 
-    if (models.length > 0) {
-        ModelSymmetry.Provider.set(models[0], symmetry);
+    if (models.frameCount > 0) {
+        const first = models.representative;
+
+        ModelSymmetry.Provider.set(first, symmetry);
 
         const bondCount = db.geom_bond._rowCount;
         if(bondCount > 0) {
@@ -179,7 +182,7 @@ async function getModels(db: CifCore_Database, format: CifCoreFormat, ctx: Runti
                 symmetryB[i] = site_symmetry_2.value(i) || '1_555';
             }
 
-            IndexPairBonds.Provider.set(models[0], IndexPairBonds.fromData({ pairs: {
+            IndexPairBonds.Provider.set(first, IndexPairBonds.fromData({ pairs: {
                 indexA: Column.ofIntArray(indexA),
                 indexB: Column.ofIntArray(indexB),
                 order: Column.ofIntArray(order),
@@ -238,7 +241,7 @@ namespace CifCoreFormat {
     }
 }
 
-export function trajectoryFromCifCore(frame: CifFrame): Task<Model.Trajectory> {
+export function trajectoryFromCifCore(frame: CifFrame): Task<Trajectory> {
     const format = CifCoreFormat.fromFrame(frame);
     return Task.create('Parse CIF Core', ctx => getModels(format.data.db, format, ctx));
 }

+ 3 - 3
src/mol-model-formats/structure/cube.ts

@@ -5,7 +5,6 @@
  */
 
 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';
@@ -14,8 +13,9 @@ import { ComponentBuilder } from './common/component';
 import { EntityBuilder } from './common/entity';
 import { ModelFormat } from '../format';
 import { CubeFile } from '../../mol-io/reader/cube/parser';
+import { Trajectory } from '../../mol-model/structure';
 
-async function getModels(cube: CubeFile, ctx: RuntimeContext): Promise<Model[]> {
+async function getModels(cube: CubeFile, ctx: RuntimeContext) {
     const { atoms } = cube;
 
     const MOL = Column.ofConst('MOL', cube.atoms.count, Column.Schema.str);
@@ -78,6 +78,6 @@ namespace MolFormat {
     }
 }
 
-export function trajectoryFromCube(cube: CubeFile): Task<Model.Trajectory> {
+export function trajectoryFromCube(cube: CubeFile): Task<Trajectory> {
     return Task.create('Parse Cube', ctx => getModels(cube, ctx));
 }

+ 7 - 3
src/mol-model-formats/structure/gro.ts

@@ -16,6 +16,8 @@ import { getChainId } from './common/util';
 import { EntityBuilder } from './common/entity';
 import { BasicData, BasicSchema, createBasic } from './basic/schema';
 import { createModels } from './basic/parser';
+import { Trajectory } from '../../mol-model/structure';
+import { ArrayTrajectory } from '../../mol-model/structure/trajectory';
 
 function getBasic(atoms: GroAtoms, modelNum: number): BasicData {
     const auth_atom_id = atoms.atomName;
@@ -116,15 +118,17 @@ namespace GroFormat {
 // TODO reuse static model parts when hierarchy is identical
 //      need to pass all gro.structures as one table into createModels
 
-export function trajectoryFromGRO(gro: GroFile): Task<Model.Trajectory> {
+export function trajectoryFromGRO(gro: GroFile): Task<Trajectory> {
     return Task.create('Parse GRO', async ctx => {
         const format = GroFormat.fromGro(gro);
         const models: Model[] = [];
         for (let i = 0, il = gro.structures.length; i < il; ++i) {
             const basic = getBasic(gro.structures[i].atoms, i + 1);
             const m = await createModels(basic, format, ctx);
-            if (m.length === 1) models.push(m[0]);
+            if (m.frameCount === 1) {
+                models.push(m.representative);
+            }
         }
-        return models;
+        return new ArrayTrajectory(models);
     });
 }

+ 2 - 1
src/mol-model-formats/structure/mmcif.ts

@@ -17,6 +17,7 @@ import { Table } from '../../mol-data/db';
 import { AtomSiteAnisotrop } from './property/anisotropic';
 import { ComponentBond } from './property/bonds/chem_comp';
 import { StructConn } from './property/bonds/struct_conn';
+import { Trajectory } from '../../mol-model/structure';
 
 function modelSymmetryFromMmcif(model: Model) {
     if (!MmcifFormat.is(model.sourceData)) return;
@@ -86,7 +87,7 @@ namespace MmcifFormat {
     }
 }
 
-export function trajectoryFromMmCIF(frame: CifFrame): Task<Model.Trajectory> {
+export function trajectoryFromMmCIF(frame: CifFrame): Task<Trajectory> {
     const format = MmcifFormat.fromFrame(frame);
     return Task.create('Create mmCIF Model', ctx => createModels(format.data.db, format, ctx));
 }

+ 5 - 5
src/mol-model-formats/structure/mol.ts

@@ -7,7 +7,6 @@
 
 import { Column, Table } from '../../mol-data/db';
 import { MolFile } from '../../mol-io/reader/mol/parser';
-import { Model } from '../../mol-model/structure/model';
 import { MoleculeType } from '../../mol-model/structure/model/types';
 import { RuntimeContext, Task } from '../../mol-task';
 import { createModels } from './basic/parser';
@@ -16,8 +15,9 @@ import { ComponentBuilder } from './common/component';
 import { EntityBuilder } from './common/entity';
 import { ModelFormat } from '../format';
 import { IndexPairBonds } from './property/bonds/index-pair';
+import { Trajectory } from '../../mol-model/structure';
 
-async function getModels(mol: MolFile, ctx: RuntimeContext): Promise<Model[]> {
+async function getModels(mol: MolFile, ctx: RuntimeContext) {
     const { atoms, bonds } = mol;
 
     const MOL = Column.ofConst('MOL', mol.atoms.count, Column.Schema.str);
@@ -63,12 +63,12 @@ async function getModels(mol: MolFile, ctx: RuntimeContext): Promise<Model[]> {
 
     const models = await createModels(basics, MolFormat.create(mol), ctx);
 
-    if (models.length > 0) {
+    if (models.frameCount > 0) {
         const indexA = Column.ofIntArray(Column.mapToArray(bonds.atomIdxA, x => x - 1, Int32Array));
         const indexB = Column.ofIntArray(Column.mapToArray(bonds.atomIdxB, x => x - 1, Int32Array));
         const order = Column.asArrayColumn(bonds.order, Int32Array);
         const pairBonds = IndexPairBonds.fromData({ pairs: { indexA, indexB, order }, count: bonds.count });
-        IndexPairBonds.Provider.set(models[0], pairBonds);
+        IndexPairBonds.Provider.set(models.representative, pairBonds);
     }
 
     return models;
@@ -90,6 +90,6 @@ namespace MolFormat {
     }
 }
 
-export function trajectoryFromMol(mol: MolFile): Task<Model.Trajectory> {
+export function trajectoryFromMol(mol: MolFile): Task<Trajectory> {
     return Task.create('Parse MOL', ctx => getModels(mol, ctx));
 }

+ 10 - 7
src/mol-model-formats/structure/mol2.ts

@@ -16,8 +16,9 @@ import { ModelFormat } from '../format';
 import { IndexPairBonds } from './property/bonds/index-pair';
 import { Mol2File } from '../../mol-io/reader/mol2/schema';
 import { AtomPartialCharge } from './property/partial-charge';
+import { Trajectory, ArrayTrajectory } from '../../mol-model/structure';
 
-async function getModels(mol2: Mol2File, ctx: RuntimeContext): Promise<Model[]> {
+async function getModels(mol2: Mol2File, ctx: RuntimeContext) {
     const models: Model[] = [];
 
     for (let i = 0, il = mol2.structures.length; i < il; ++i) {
@@ -64,23 +65,25 @@ async function getModels(mol2: Mol2File, ctx: RuntimeContext): Promise<Model[]>
 
         const _models = await createModels(basics, Mol2Format.create(mol2), ctx);
 
-        if (_models.length > 0) {
+        if (_models.frameCount > 0) {
             const indexA = Column.ofIntArray(Column.mapToArray(bonds.origin_atom_id, x => x - 1, Int32Array));
             const indexB = Column.ofIntArray(Column.mapToArray(bonds.target_atom_id, x => x - 1, Int32Array));
             const order = Column.ofIntArray(Column.mapToArray(bonds.bond_type, x => x === 'ar' ? 1 : parseInt(x), Int8Array));
             const pairBonds = IndexPairBonds.fromData({ pairs: { indexA, indexB, order }, count: bonds.count });
-            IndexPairBonds.Provider.set(_models[0], pairBonds);
 
-            AtomPartialCharge.Provider.set(_models[0], {
+            const first = _models.representative;
+            IndexPairBonds.Provider.set(first, pairBonds);
+
+            AtomPartialCharge.Provider.set(first, {
                 data: atoms.charge,
                 type: molecule.charge_type
             });
 
-            models.push(_models[0]);
+            models.push(first);
         }
     }
 
-    return models;
+    return new ArrayTrajectory(models);
 }
 
 //
@@ -99,6 +102,6 @@ namespace Mol2Format {
     }
 }
 
-export function trajectoryFromMol2(mol2: Mol2File): Task<Model.Trajectory> {
+export function trajectoryFromMol2(mol2: Mol2File): Task<Trajectory> {
     return Task.create('Parse MOL2', ctx => getModels(mol2, ctx));
 }

+ 6 - 5
src/mol-model-formats/structure/pdb.ts

@@ -7,14 +7,14 @@
 
 import { PdbFile } from '../../mol-io/reader/pdb/schema';
 import { pdbToMmCif } from './pdb/to-cif';
-import { Model } from '../../mol-model/structure/model';
 import { Task } from '../../mol-task';
 import { MmcifFormat } from './mmcif';
 import { createModels } from './basic/parser';
 import { Column } from '../../mol-data/db';
 import { AtomPartialCharge } from './property/partial-charge';
+import { Trajectory } from '../../mol-model/structure';
 
-export function trajectoryFromPDB(pdb: PdbFile): Task<Model.Trajectory> {
+export function trajectoryFromPDB(pdb: PdbFile): Task<Trajectory> {
     return Task.create('Parse PDB', async ctx => {
         await ctx.update('Converting to mmCIF');
         const cif = await pdbToMmCif(pdb);
@@ -24,8 +24,9 @@ export function trajectoryFromPDB(pdb: PdbFile): Task<Model.Trajectory> {
         if (partial_charge) {
             // TODO works only for single, unsorted model, to work generally
             //      would need to do model splitting again
-            if (models.length === 1) {
-                const srcIndex = models[0].atomicHierarchy.atoms.sourceIndex;
+            if (models.frameCount === 1) {
+                const first = models.representative;
+                const srcIndex = first.atomicHierarchy.atoms.sourceIndex;
                 const isIdentity = Column.isIdentity(srcIndex);
                 const srcIndexArray = isIdentity ? void 0 : srcIndex.toArray({ array: Int32Array });
 
@@ -34,7 +35,7 @@ export function trajectoryFromPDB(pdb: PdbFile): Task<Model.Trajectory> {
                     ? Column.ofFloatArray(Column.mapToArray(srcIndex, i => q[i], Float32Array))
                     : Column.ofFloatArray(q);
 
-                AtomPartialCharge.Provider.set(models[0], {
+                AtomPartialCharge.Provider.set(first, {
                     data: partialCharge,
                     type: 'GASTEIGER' // from PDBQT
                 });

+ 2 - 1
src/mol-model/structure.ts

@@ -9,4 +9,5 @@ export * from './structure/coordinates';
 export * from './structure/topology';
 export * from './structure/model';
 export * from './structure/structure';
-export * from './structure/query';
+export * from './structure/query';
+export * from './structure/trajectory';

+ 7 - 8
src/mol-model/structure/model/model.ts

@@ -15,7 +15,6 @@ import { SaccharideComponentMap } from '../structure/carbohydrates/constants';
 import { ModelFormat } from '../../../mol-model-formats/format';
 import { calcModelCenter, getAsymIdCount } from './util';
 import { Vec3 } from '../../../mol-math/linear-algebra';
-import { Mutable } from '../../../mol-util/type-helpers';
 import { Coordinates } from '../coordinates';
 import { Topology } from '../topology';
 import { Task } from '../../../mol-task';
@@ -27,6 +26,7 @@ import { SymmetryOperator } from '../../../mol-math/geometry';
 import { ModelSymmetry } from '../../../mol-model-formats/structure/property/symmetry';
 import { Column } from '../../../mol-data/db';
 import { CustomModelProperty } from '../../../mol-model-props/common/custom-model-property';
+import { Trajectory, ArrayTrajectory } from '../trajectory';
 
 /**
  * Interface to the "source data" of the molecule.
@@ -86,10 +86,8 @@ export interface Model extends Readonly<{
 } { }
 
 export namespace Model {
-    export type Trajectory = ReadonlyArray<Model>
-
     function _trajectoryFromModelAndCoordinates(model: Model, coordinates: Coordinates) {
-        const trajectory: Mutable<Model.Trajectory> = [];
+        const trajectory: Model[] = [];
         const { frames } = coordinates;
 
         const srcIndex = model.atomicHierarchy.atoms.sourceIndex;
@@ -117,7 +115,7 @@ export namespace Model {
     }
 
     export function trajectoryFromModelAndCoordinates(model: Model, coordinates: Coordinates): Trajectory {
-        return _trajectoryFromModelAndCoordinates(model, coordinates).trajectory;
+        return new ArrayTrajectory(_trajectoryFromModelAndCoordinates(model, coordinates).trajectory);
     }
 
     export function invertIndex(xs: ArrayLike<number>) {
@@ -130,8 +128,9 @@ export namespace Model {
 
     export function trajectoryFromTopologyAndCoordinates(topology: Topology, coordinates: Coordinates): Task<Trajectory> {
         return Task.create('Create Trajectory', async ctx => {
-            const model = (await createModels(topology.basic, topology.sourceData, ctx))[0];
-            if (!model) throw new Error('found no model');
+            const models = await createModels(topology.basic, topology.sourceData, ctx);
+            if (models.frameCount === 0) throw new Error('found no model');
+            const model = models.representative;
             const { trajectory, srcIndexArray } = _trajectoryFromModelAndCoordinates(model, coordinates);
 
             // TODO: cache the inverted index somewhere?
@@ -152,7 +151,7 @@ export namespace Model {
                 IndexPairBonds.Provider.set(m, indexPairBonds);
                 TrajectoryInfo.set(m, { index: index++, size: trajectory.length });
             }
-            return trajectory;
+            return new ArrayTrajectory(trajectory);
         });
     }
 

+ 10 - 5
src/mol-model/structure/structure/structure.ts

@@ -31,6 +31,8 @@ import { StructureSelection } from '../query/selection';
 import { getBoundary } from '../../../mol-math/geometry/boundary';
 import { ElementSymbol } from '../model/types';
 import { CustomStructureProperty } from '../../../mol-model-props/common/custom-structure-property';
+import { Trajectory } from '../trajectory';
+import { RuntimeContext, Task } from '../../../mol-task';
 
 class Structure {
     /** Maps unit.id to unit */
@@ -639,14 +641,17 @@ namespace Structure {
         return new Structure(units, props);
     }
 
-    export function ofTrajectory(trajectory: ReadonlyArray<Model>): Structure {
-        if (trajectory.length === 0) return Empty;
+    export async function ofTrajectory(trajectory: Trajectory, ctx: RuntimeContext): Promise<Structure> {
+        if (trajectory.frameCount === 0) return Empty;
 
         const units: Unit[] = [];
 
+        let first: Model | undefined = void 0;
         let count = 0;
-        for (let i = 0, il = trajectory.length; i < il; ++i) {
-            const structure = ofModel(trajectory[i]);
+        for (let i = 0, il = trajectory.frameCount; i < il; ++i) {
+            const frame = await Task.resolveInContext(trajectory.getFrameAtIndex(i), ctx);
+            if (!first) first = frame;
+            const structure = ofModel(frame);
             for (let j = 0, jl = structure.units.length; j < jl; ++j) {
                 const u = structure.units[j];
                 const invariantId = u.invariantId + count;
@@ -657,7 +662,7 @@ namespace Structure {
             count = units.length;
         }
 
-        return create(units, { representativeModel: trajectory[0], label: trajectory[0].label });
+        return create(units, { representativeModel: first!, label: first!.label });
     }
 
     const PARTITION = false;

+ 45 - 0
src/mol-model/structure/trajectory.ts

@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { Task } from '../../mol-task';
+import { Model } from '../structure';
+
+export type TrajectoryFrameType =
+  | { type: 'default' }
+  /** Returns the closest available frame to the requested index  */
+  | { type: 'snap' }
+  /** Interpolates between two available adjacent frames */
+  | { type: 'interpolate', kind?: 'linear' }
+
+/**
+ * A generic interface for representing (partial) trajectories
+ */
+export interface Trajectory {
+    readonly duration: number,
+    readonly frameCount: number,
+
+    /** Statically available representative model. Required for example by certain UI actions. */
+    readonly representative: Model,
+
+    /** Allows to asynchronously query data from a server or interpolate frames on the fly */
+    getFrameAtIndex(i: number, type?: TrajectoryFrameType): Task<Model> | Model
+}
+
+export class ArrayTrajectory implements Trajectory {
+    readonly duration: number;
+    readonly frameCount: number;
+    readonly representative: Model;
+
+    getFrameAtIndex(i: number) {
+        return this.frames[i];
+    }
+
+    constructor(private frames: Model[]) {
+        this.frameCount = frames.length;
+        this.representative = frames[0];
+        this.duration = frames.length;
+    }
+}

+ 2 - 2
src/mol-plugin-state/actions/structure.ts

@@ -191,8 +191,8 @@ export const UpdateTrajectory = StateAction.build({
             if (!parent || !parent.obj) continue;
             const traj = parent.obj;
             update.to(m).update(old => {
-                let modelIndex = (old.modelIndex + params.by!) % traj.data.length;
-                if (modelIndex < 0) modelIndex += traj.data.length;
+                let modelIndex = (old.modelIndex + params.by!) % traj.data.frameCount;
+                if (modelIndex < 0) modelIndex += traj.data.frameCount;
                 return { modelIndex };
             });
         }

+ 3 - 3
src/mol-plugin-state/animation/built-in.ts

@@ -28,7 +28,7 @@ export const AnimateModelIndex = PluginStateAnimation.create({
         const models = state.select(StateSelection.Generators.ofTransformer(StateTransforms.Model.ModelFromTrajectory));
         for (const m of models) {
             const parent = StateSelection.findAncestorOfType(state.tree, state.cells, m.transform.ref, [PluginStateObject.Molecule.Trajectory]);
-            if (parent && parent.obj && parent.obj.data.length > 1) return { canApply: true };
+            if (parent && parent.obj && parent.obj.data.frameCount > 1) return { canApply: true };
         }
         return { canApply: false, reason: 'No trajectory to animate' };
     },
@@ -57,10 +57,10 @@ export const AnimateModelIndex = PluginStateAnimation.create({
             const parent = StateSelection.findAncestorOfType(state.tree, state.cells, m.transform.ref, [PluginStateObject.Molecule.Trajectory]);
             if (!parent || !parent.obj) continue;
             const traj = parent.obj;
-            if (traj.data.length <= 1) continue;
+            if (traj.data.frameCount <= 1) continue;
 
             update.to(m).update(old => {
-                const len = traj.data.length;
+                const len = traj.data.frameCount;
                 if (len !== 1) {
                     allSingles = false;
                 } else {

+ 6 - 6
src/mol-plugin-state/builder/structure/hierarchy-preset.ts

@@ -80,7 +80,7 @@ const allModels = TrajectoryHierarchyPresetProvider({
         description: 'Shows all models; colored by model-index.'
     },
     isApplicable: o => {
-        return o.data.length > 1;
+        return o.data.frameCount > 1;
     },
     params: CommonParams,
     async apply(trajectory, params, plugin) {
@@ -91,7 +91,7 @@ const allModels = TrajectoryHierarchyPresetProvider({
 
         const models = [], structures = [];
 
-        for (let i = 0; i < tr.length; i++) {
+        for (let i = 0; i < tr.frameCount; i++) {
             const model = await builder.createModel(trajectory, { modelIndex: i });
             const modelProperties = await builder.insertModelProperties(model, params.modelProperties, { isCollapsed: true });
             const structure = await builder.createStructure(modelProperties || model, { name: 'model', params: {} });
@@ -100,7 +100,7 @@ const allModels = TrajectoryHierarchyPresetProvider({
             models.push(model);
             structures.push(structure);
 
-            const quality = structure.obj ? getStructureQuality(structure.obj.data, { elementCountFactor: tr.length }) : 'medium';
+            const quality = structure.obj ? getStructureQuality(structure.obj.data, { elementCountFactor: tr.frameCount }) : 'medium';
             await builder.representation.applyPreset(structureProperties, params.representationPreset || 'auto', { theme: { globalName: 'model-index' }, quality });
         }
 
@@ -145,7 +145,7 @@ const unitcell = TrajectoryHierarchyPresetProvider({
         description: 'Shows the fully populated unit cell.'
     },
     isApplicable: o => {
-        return Model.hasCrystalSymmetry(o.data[0]);
+        return Model.hasCrystalSymmetry(o.data.representative);
     },
     params: CrystalSymmetryParams,
     async apply(trajectory, params, plugin) {
@@ -160,7 +160,7 @@ const supercell = TrajectoryHierarchyPresetProvider({
         description: 'Shows the super cell, i.e. the central unit cell and all adjacent unit cells.'
     },
     isApplicable: o => {
-        return Model.hasCrystalSymmetry(o.data[0]);
+        return Model.hasCrystalSymmetry(o.data.representative);
     },
     params: CrystalSymmetryParams,
     async apply(trajectory, params, plugin) {
@@ -180,7 +180,7 @@ const crystalContacts = TrajectoryHierarchyPresetProvider({
         description: 'Showsasymetric unit and chains from neighbours within 5 \u212B, i.e., symmetry mates.'
     },
     isApplicable: o => {
-        return Model.hasCrystalSymmetry(o.data[0]);
+        return Model.hasCrystalSymmetry(o.data.representative);
     },
     params: CrystalContactsParams,
     async apply(trajectory, params, plugin) {

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

@@ -13,7 +13,7 @@ import { Dsn6File } from '../mol-io/reader/dsn6/schema';
 import { PlyFile } from '../mol-io/reader/ply/schema';
 import { PsfFile } from '../mol-io/reader/psf/parser';
 import { ShapeProvider } from '../mol-model/shape/provider';
-import { Coordinates as _Coordinates, Model as _Model, Structure as _Structure, StructureElement, Topology as _Topology } from '../mol-model/structure';
+import { Coordinates as _Coordinates, Model as _Model, Structure as _Structure, Trajectory as _Trajectory, StructureElement, Topology as _Topology } from '../mol-model/structure';
 import { Volume as _Volume } from '../mol-model/volume';
 import { PluginBehavior } from '../mol-plugin/behavior/behavior';
 import { Representation } from '../mol-repr/representation';
@@ -100,7 +100,7 @@ export namespace PluginStateObject {
         export class Coordinates extends Create<_Coordinates>({ name: 'Coordinates', typeClass: 'Object' }) { }
         export class Topology extends Create<_Topology>({ name: 'Topology', typeClass: 'Object' }) { }
         export class Model extends Create<_Model>({ name: 'Model', typeClass: 'Object' }) { }
-        export class Trajectory extends Create<ReadonlyArray<_Model>>({ name: 'Trajectory', typeClass: 'Object' }) { }
+        export class Trajectory extends Create<_Trajectory>({ name: 'Trajectory', typeClass: 'Object' }) { }
         export class Structure extends Create<_Structure>({ name: 'Structure', typeClass: 'Object' }) { }
 
         export namespace Structure {

+ 35 - 24
src/mol-plugin-state/transforms/model.ts

@@ -17,7 +17,7 @@ import { trajectoryFromGRO } from '../../mol-model-formats/structure/gro';
 import { trajectoryFromMmCIF } from '../../mol-model-formats/structure/mmcif';
 import { trajectoryFromPDB } from '../../mol-model-formats/structure/pdb';
 import { topologyFromPsf } from '../../mol-model-formats/structure/psf';
-import { Coordinates, Model, Queries, QueryContext, Structure, StructureElement, StructureQuery, StructureSelection as Sel, Topology } from '../../mol-model/structure';
+import { Coordinates, Model, Queries, QueryContext, Structure, StructureElement, StructureQuery, StructureSelection as Sel, Topology, ArrayTrajectory, Trajectory } from '../../mol-model/structure';
 import { PluginContext } from '../../mol-plugin/context';
 import { MolScriptBuilder } from '../../mol-script/language/builder';
 import Expression from '../../mol-script/language/expression';
@@ -142,7 +142,7 @@ const TrajectoryFromModelAndCoordinates = PluginStateTransform.BuiltIn({
         return Task.create('Create trajectory from model/topology and coordinates', async ctx => {
             const coordinates = dependencies![params.coordinatesRef].data as Coordinates;
             const trajectory = await getTrajectory(ctx, dependencies![params.modelRef], coordinates);
-            const props = { label: 'Trajectory', description: `${trajectory.length} model${trajectory.length === 1 ? '' : 's'}` };
+            const props = { label: 'Trajectory', description: `${trajectory.frameCount} model${trajectory.frameCount === 1 ? '' : 's'}` };
             return new SO.Molecule.Trajectory(trajectory, props);
         });
     }
@@ -162,16 +162,25 @@ const TrajectoryFromBlob = PluginStateTransform.BuiltIn({
                 if (e.kind !== 'cif') continue;
                 const block = e.data.blocks[0];
                 const xs = await trajectoryFromMmCIF(block).runInContext(ctx);
-                if (xs.length === 0) throw new Error('No models found.');
-                for (const x of xs) models.push(x);
+                if (xs.frameCount === 0) throw new Error('No models found.');
+
+                for (let i = 0; i < xs.frameCount; i++) {
+                    const x = await Task.resolveInContext(xs.getFrameAtIndex(i), ctx);
+                    models.push(x);
+                }
             }
 
             const props = { label: 'Trajectory', description: `${models.length} model${models.length === 1 ? '' : 's'}` };
-            return new SO.Molecule.Trajectory(models, props);
+            return new SO.Molecule.Trajectory(new ArrayTrajectory(models), props);
         });
     }
 });
 
+function trajectoryProps(trajectory: Trajectory) {
+    const first = trajectory.representative;
+    return { label: `${first.entry}`, description: `${trajectory.frameCount} model${trajectory.frameCount === 1 ? '' : 's'}` };
+}
+
 type TrajectoryFromMmCif = typeof TrajectoryFromMmCif
 const TrajectoryFromMmCif = PluginStateTransform.BuiltIn({
     name: 'trajectory-from-mmcif',
@@ -197,8 +206,8 @@ const TrajectoryFromMmCif = PluginStateTransform.BuiltIn({
             const block = a.data.blocks.find(b => b.header === header);
             if (!block) throw new Error(`Data block '${[header]}' not found.`);
             const models = await trajectoryFromMmCIF(block).runInContext(ctx);
-            if (models.length === 0) throw new Error('No models found.');
-            const props = { label: `${models[0].entry}`, description: `${models.length} model${models.length === 1 ? '' : 's'}` };
+            if (models.frameCount === 0) throw new Error('No models found.');
+            const props = trajectoryProps(models);
             return new SO.Molecule.Trajectory(models, props);
         });
     }
@@ -219,7 +228,7 @@ const TrajectoryFromPDB = PluginStateTransform.BuiltIn({
             const parsed = await parsePDB(a.data, a.label, params.isPdbqt).runInContext(ctx);
             if (parsed.isError) throw new Error(parsed.message);
             const models = await trajectoryFromPDB(parsed.result).runInContext(ctx);
-            const props = { label: `${models[0].entry}`, description: `${models.length} model${models.length === 1 ? '' : 's'}` };
+            const props = trajectoryProps(models);
             return new SO.Molecule.Trajectory(models, props);
         });
     }
@@ -237,7 +246,7 @@ const TrajectoryFromGRO = PluginStateTransform.BuiltIn({
             const parsed = await parseGRO(a.data).runInContext(ctx);
             if (parsed.isError) throw new Error(parsed.message);
             const models = await trajectoryFromGRO(parsed.result).runInContext(ctx);
-            const props = { label: `${models[0].entry}`, description: `${models.length} model${models.length === 1 ? '' : 's'}` };
+            const props = trajectoryProps(models);
             return new SO.Molecule.Trajectory(models, props);
         });
     }
@@ -255,7 +264,7 @@ const TrajectoryFromMOL = PluginStateTransform.BuiltIn({
             const parsed = await parseMol(a.data).runInContext(ctx);
             if (parsed.isError) throw new Error(parsed.message);
             const models = await trajectoryFromMol(parsed.result).runInContext(ctx);
-            const props = { label: `${models[0].entry}`, description: `${models.length} model${models.length === 1 ? '' : 's'}` };
+            const props = trajectoryProps(models);
             return new SO.Molecule.Trajectory(models, props);
         });
     }
@@ -273,7 +282,7 @@ const TrajectoryFromMOL2 = PluginStateTransform.BuiltIn({
             const parsed = await parseMol2(a.data, a.label).runInContext(ctx);
             if (parsed.isError) throw new Error(parsed.message);
             const models = await trajectoryFromMol2(parsed.result).runInContext(ctx);
-            const props = { label: `${models[0].entry}`, description: `${models.length} model${models.length === 1 ? '' : 's'}` };
+            const props = trajectoryProps(models);
             return new SO.Molecule.Trajectory(models, props);
         });
     }
@@ -289,7 +298,7 @@ const TrajectoryFromCube = PluginStateTransform.BuiltIn({
     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'}` };
+            const props = trajectoryProps(models);
             return new SO.Molecule.Trajectory(models, props);
         });
     }
@@ -319,8 +328,8 @@ const TrajectoryFromCifCore = PluginStateTransform.BuiltIn({
             const block = a.data.blocks.find(b => b.header === header);
             if (!block) throw new Error(`Data block '${[header]}' not found.`);
             const models = await trajectoryFromCifCore(block).runInContext(ctx);
-            if (models.length === 0) throw new Error('No models found.');
-            const props = { label: `${models[0].entry}`, description: `${models.length} model${models.length === 1 ? '' : 's'}` };
+            if (models.frameCount === 0) throw new Error('No models found.');
+            const props = trajectoryProps(models);
             return new SO.Molecule.Trajectory(models, props);
         });
     }
@@ -338,7 +347,7 @@ const TrajectoryFrom3DG = PluginStateTransform.BuiltIn({
             const parsed = await parse3DG(a.data).runInContext(ctx);
             if (parsed.isError) throw new Error(parsed.message);
             const models = await trajectoryFrom3DG(parsed.result).runInContext(ctx);
-            const props = { label: `${models[0].entry}`, description: `${models.length} model${models.length === 1 ? '' : 's'}` };
+            const props = trajectoryProps(models);
             return new SO.Molecule.Trajectory(models, props);
         });
     }
@@ -355,17 +364,19 @@ const ModelFromTrajectory = PluginStateTransform.BuiltIn({
         if (!a) {
             return { modelIndex: PD.Numeric(0, {}, { description: 'Zero-based index of the model' }) };
         }
-        return { modelIndex: PD.Converted(plus1, minus1, PD.Numeric(1, { min: 1, max: a.data.length, step: 1 }, { description: 'Model Index' })) };
+        return { modelIndex: PD.Converted(plus1, minus1, PD.Numeric(1, { min: 1, max: a.data.frameCount, step: 1 }, { description: 'Model Index' })) };
     }
 })({
-    isApplicable: a => a.data.length > 0,
+    isApplicable: a => a.data.frameCount > 0,
     apply({ a, params }) {
-        let modelIndex = params.modelIndex % a.data.length;
-        if (modelIndex < 0) modelIndex += a.data.length;
-        const model = a.data[params.modelIndex];
-        const label = `Model ${model.modelNum}`;
-        const description = a.data.length === 1 ? undefined : `of ${a.data.length}`;
-        return new SO.Molecule.Model(model, { label, description });
+        return Task.create('Model from Trajectory', async ctx => {
+            let modelIndex = params.modelIndex % a.data.frameCount;
+            if (modelIndex < 0) modelIndex += a.data.frameCount;
+            const model = await Task.resolveInContext(a.data.getFrameAtIndex(modelIndex), ctx);
+            const label = `Model ${modelIndex + 1}`;
+            let description = a.data.frameCount === 1 ? undefined : `of ${a.data.frameCount}`;
+            return new SO.Molecule.Model(model, { label, description });
+        });
     },
     dispose({ b }) {
         b?.data.customProperties.dispose();
@@ -381,7 +392,7 @@ const StructureFromTrajectory = PluginStateTransform.BuiltIn({
 })({
     apply({ a }) {
         return Task.create('Build Structure', async ctx => {
-            const s = Structure.ofTrajectory(a.data);
+            const s = await Structure.ofTrajectory(a.data, ctx);
             const props = { label: 'Ensemble', description: Structure.elementDescription(s) };
             return new SO.Molecule.Structure(s, props);
         });

+ 2 - 2
src/mol-plugin-ui/controls.tsx

@@ -44,7 +44,7 @@ export class TrajectoryViewportControls extends PluginUIComponent<{}, { show: bo
             const parent = state.cells.get(m.sourceRef)!.obj as PluginStateObject.Molecule.Trajectory;
 
             if (!parent) continue;
-            if (parent.data.length > 1) {
+            if (parent.data.frameCount > 1) {
                 if (parents.has(m.sourceRef)) {
                     // do not show the controls if there are 2 models of the same trajectory present
                     this.setState({ show: false });
@@ -55,7 +55,7 @@ export class TrajectoryViewportControls extends PluginUIComponent<{}, { show: bo
                 count++;
                 if (!label) {
                     const idx = (m.transform.params! as StateTransformer.Params<ModelFromTrajectory>).modelIndex;
-                    label = `Model ${idx + 1} / ${parent.data.length}`;
+                    label = `Model ${idx + 1} / ${parent.data.frameCount}`;
                 }
             }
         }

+ 5 - 0
src/mol-plugin/context.ts

@@ -62,6 +62,11 @@ import { filter, take } from 'rxjs/operators';
 
 export class PluginContext {
     runTask = <T>(task: Task<T>) => this.tasks.run(task);
+    resolveTask = <T>(object: Task<T> | T | undefined) => {
+        if (!object) return void 0;
+        if (Task.is(object)) return this.runTask(object);
+        return object;
+    }
 
     private disposed = false;
     private ev = RxEventHelper.create();

+ 5 - 0
src/mol-task/task.ts

@@ -71,6 +71,11 @@ namespace Task {
     export function empty(): Task<void> { return create('', async ctx => {}); }
     export function fail(name: string, reason: string): Task<any> { return create(name, async ctx => { throw new Error(reason); }); }
 
+    export function resolveInContext<T>(object: Task<T> | T, ctx?: RuntimeContext) {
+        if (is(object)) return ctx ? object.runInContext(ctx) : object.run();
+        return object;
+    }
+
     export interface Progress {
         taskId: number,
         taskName: string,

+ 2 - 2
src/perf-tests/lookup3d.ts

@@ -34,9 +34,9 @@ export async function readCIF(path: string) {
     }
 
     const models = await trajectoryFromMmCIF(parsed.result.blocks[0]).run();
-    const structures = models.map(Structure.ofModel);
+    const structures = [Structure.ofModel(models.representative)];
 
-    return { mmcif: models[0].sourceData.data as MmcifFormat.Data, models, structures };
+    return { mmcif: models.representative.sourceData.data as MmcifFormat.Data, models, structures };
 }
 
 export async function test() {

+ 4 - 4
src/perf-tests/structure.ts

@@ -73,9 +73,9 @@ export async function readCIF(path: string) {
     console.time('buildModels');
     const models = await trajectoryFromMmCIF(data).run();
     console.timeEnd('buildModels');
-    const structures = models.map(Structure.ofModel);
+    const structures = [Structure.ofModel(models.representative)];
 
-    return { mmcif: models[0].sourceData.data, models, structures };
+    return { mmcif: models.representative.sourceData.data, models, structures };
 }
 
 const DATA_DIR = './build/data';
@@ -411,7 +411,7 @@ export namespace PropertyAccess {
 
         // return;
 
-        console.log('bs', baseline(models[0]));
+        console.log('bs', baseline(models.representative));
         console.log('sp', sumProperty(structures[0], l => l.unit.model.atomicConformation.atomId.value(l.element)));
         // console.log(sumPropertySegmented(structures[0], l => l.unit.model.atomSiteConformation.atomId.value(l.element)));
 
@@ -459,7 +459,7 @@ export namespace PropertyAccess {
         console.log(StructureSelection.structureCount(q2r));
         // console.log(q1(structures[0]));
 
-        const col = models[0].atomicConformation.atomId.value;
+        const col = models.representative.atomicConformation.atomId.value;
         const suite = new B.Suite();
         suite
             // .add('test q', () => q1(structures[0]))

+ 7 - 2
src/servers/model/server/structure-wrapper.ts

@@ -17,6 +17,7 @@ import { ConsoleLogger } from '../../../mol-util/console-logger';
 import { ModelPropertiesProvider } from '../property-provider';
 import { trajectoryFromMmCIF } from '../../../mol-model-formats/structure/mmcif';
 import { fetchRetry } from '../utils/fetch-retry';
+import { Task } from '../../../mol-task';
 
 require('util.promisify').shim();
 
@@ -160,11 +161,15 @@ function readOrFetch(jobId: string, key: string, sourceId: string | '_local_', e
 export async function readStructureWrapper(key: string, sourceId: string | '_local_', entryId: string, jobId: string | undefined, propertyProvider: ModelPropertiesProvider | undefined) {
     const { data, frame, isBinary } = await readOrFetch(jobId || '', key, sourceId, entryId);
     perf.start('createModel');
-    const models = await trajectoryFromMmCIF(frame).run();
+    const trajectory = await trajectoryFromMmCIF(frame).run();
     perf.end('createModel');
 
+    const models: Model[] = [];
     const modelMap = new Map<number, Model>();
-    for (const m of models) {
+
+    for (let i = 0; i < trajectory.frameCount; i++) {
+        const m = await Task.resolveInContext(trajectory.getFrameAtIndex(i));
+        models.push(m);
         modelMap.set(m.modelNum, m);
     }
 

+ 1 - 1
src/tests/browser/render-structure.ts

@@ -127,7 +127,7 @@ async function init() {
 
     const cif = await downloadFromPdb('3pqr');
     const models = await getModels(cif);
-    const structure = await getStructure(models[0]);
+    const structure = await getStructure(models.representative);
 
     console.time('compute SecondaryStructure');
     await SecondaryStructureProvider.attach(ctx, structure);