Browse Source

trajectory from topology and coordinates

Alexander Rose 5 years ago
parent
commit
5d5ffcdb36

+ 2 - 2
src/mol-model-formats/structure/gro.ts

@@ -105,7 +105,7 @@ function getCategories(atoms: GroAtoms) {
     }
 }
 
-async function groToMmCif(gro: GroFile) {
+function groToMmCif(gro: GroFile) {
     const categories = getCategories(gro.structures[0].atoms)
 
     return {
@@ -118,7 +118,7 @@ async function groToMmCif(gro: GroFile) {
 export function trajectoryFromGRO(gro: GroFile): Task<Model.Trajectory> {
     return Task.create('Parse GRO', async ctx => {
         await ctx.update('Converting to mmCIF');
-        const cif = await groToMmCif(gro);
+        const cif = groToMmCif(gro);
         const format = ModelFormat.mmCIF(cif);
         return _parse_mmCif(format, ctx);
     })

+ 8 - 5
src/mol-model-formats/structure/mmcif/atomic.ts

@@ -97,7 +97,7 @@ function isHierarchyDataEqual(a: AtomicData, b: AtomicData) {
         && Table.areEqual(a.atoms as Table<AtomsSchema>, b.atoms as Table<AtomsSchema>)
 }
 
-export function getAtomicHierarchyAndConformation(atom_site: AtomSite, sourceIndex: Column<number>, entities: Entities, formatData: FormatData, previous?: Model) {
+function getAtomicHierarchy(atom_site: AtomSite, sourceIndex: Column<number>, entities: Entities, formatData: FormatData, previous?: Model) {
     const hierarchyOffsets = findHierarchyOffsets(atom_site);
     const hierarchyData = createHierarchyData(atom_site, sourceIndex, hierarchyOffsets);
 
@@ -105,12 +105,9 @@ export function getAtomicHierarchyAndConformation(atom_site: AtomSite, sourceInd
         return {
             sameAsPrevious: true,
             hierarchy: previous.atomicHierarchy,
-            conformation: getConformation(atom_site)
         };
     }
 
-    const conformation = getConformation(atom_site)
-
     const hierarchySegments: AtomicSegments = {
         residueAtomSegments: Segmentation.ofOffsets(hierarchyOffsets.residues, Interval.ofBounds(0, atom_site._rowCount)),
         chainAtomSegments: Segmentation.ofOffsets(hierarchyOffsets.chains, Interval.ofBounds(0, atom_site._rowCount)),
@@ -119,5 +116,11 @@ export function getAtomicHierarchyAndConformation(atom_site: AtomSite, sourceInd
     const index = getAtomicIndex(hierarchyData, entities, hierarchySegments);
     const derived = getAtomicDerivedData(hierarchyData, index, formatData.chemicalComponentMap);
     const hierarchy: AtomicHierarchy = { ...hierarchyData, ...hierarchySegments, index, derived };
-    return { sameAsPrevious: false, hierarchy, conformation };
+    return { sameAsPrevious: false, hierarchy };
+}
+
+export function getAtomicHierarchyAndConformation(atom_site: AtomSite, sourceIndex: Column<number>, entities: Entities, formatData: FormatData, previous?: Model) {
+    const { sameAsPrevious, hierarchy } = getAtomicHierarchy(atom_site, sourceIndex, entities, formatData, previous)
+    const conformation = getConformation(atom_site)
+    return { sameAsPrevious, hierarchy, conformation };
 }

+ 135 - 0
src/mol-model-formats/structure/psf.ts

@@ -0,0 +1,135 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { PsfFile } from '../../mol-io/reader/psf/parser';
+import { mmCIF_Schema } from '../../mol-io/reader/cif/schema/mmcif';
+import { Column } from '../../mol-data/db';
+import { EntityBuilder } from './common/entity';
+import { ComponentBuilder } from './common/component';
+import { CifCategory, CifField } from '../../mol-io/reader/cif';
+import { guessElementSymbolString } from './util';
+import { MoleculeType, getMoleculeType } from '../../mol-model/structure/model/types';
+import { getChainId } from './common/util';
+import { Task } from '../../mol-task';
+import { ModelFormat } from './format';
+import { Topology } from '../../mol-model/structure/topology/topology';
+
+// TODO: shares most of the code with ./gro.ts#getCategories
+function getCategories(atoms: PsfFile['atoms']) {
+    const auth_atom_id = CifField.ofColumn(atoms.atomName)
+    const auth_comp_id = CifField.ofColumn(atoms.residueName)
+
+    const entityIds = new Array<string>(atoms.count)
+    const asymIds = new Array<string>(atoms.count)
+    const seqIds = new Uint32Array(atoms.count)
+    const ids = new Uint32Array(atoms.count)
+
+    const entityBuilder = new EntityBuilder()
+    const componentBuilder = new ComponentBuilder(atoms.residueId, atoms.atomName)
+
+    let currentEntityId = ''
+    let currentAsymIndex = 0
+    let currentAsymId = ''
+    let currentSeqId = 0
+    let prevMoleculeType = MoleculeType.Unknown
+    let prevResidueNumber = -1
+
+    for (let i = 0, il = atoms.count; i < il; ++i) {
+        const residueNumber = atoms.residueId.value(i)
+        if (residueNumber !== prevResidueNumber) {
+            const compId = atoms.residueName.value(i)
+            const moleculeType = getMoleculeType(componentBuilder.add(compId, i).type, compId)
+
+            if (moleculeType !== prevMoleculeType || residueNumber !== prevResidueNumber + 1) {
+                currentAsymId = getChainId(currentAsymIndex)
+                currentAsymIndex += 1
+                currentSeqId = 0
+            }
+
+            currentEntityId = entityBuilder.getEntityId(compId, moleculeType, currentAsymId)
+            currentSeqId += 1
+
+            prevResidueNumber = residueNumber
+            prevMoleculeType = moleculeType
+        }
+
+        entityIds[i] = currentEntityId
+        asymIds[i] = currentAsymId
+        seqIds[i] = currentSeqId
+        ids[i] = i
+    }
+
+    const auth_asym_id = CifField.ofColumn(Column.ofStringArray(asymIds))
+
+    const atom_site: CifCategory.SomeFields<mmCIF_Schema['atom_site']> = {
+        auth_asym_id,
+        auth_atom_id,
+        auth_comp_id,
+        auth_seq_id: CifField.ofColumn(atoms.residueId),
+        B_iso_or_equiv: CifField.ofColumn(Column.Undefined(atoms.count, Column.Schema.float)),
+        Cartn_x: CifField.ofColumn(Column.Undefined(atoms.count, Column.Schema.float)),
+        Cartn_y: CifField.ofColumn(Column.Undefined(atoms.count, Column.Schema.float)),
+        Cartn_z: CifField.ofColumn(Column.Undefined(atoms.count, Column.Schema.float)),
+        group_PDB: CifField.ofColumn(Column.Undefined(atoms.count, Column.Schema.str)),
+        id: CifField.ofColumn(Column.ofIntArray(ids)),
+
+        label_alt_id: CifField.ofColumn(Column.Undefined(atoms.count, Column.Schema.str)),
+
+        label_asym_id: auth_asym_id,
+        label_atom_id: auth_atom_id,
+        label_comp_id: auth_comp_id,
+        label_seq_id: CifField.ofColumn(Column.ofIntArray(seqIds)),
+        label_entity_id: CifField.ofColumn(Column.ofStringArray(entityIds)),
+
+        occupancy: CifField.ofColumn(Column.ofConst(1, atoms.count, Column.Schema.float)),
+        type_symbol: CifField.ofStrings(Column.mapToArray(atoms.atomName, s => guessElementSymbolString(s))),
+
+        pdbx_PDB_ins_code: CifField.ofColumn(Column.Undefined(atoms.count, Column.Schema.str)),
+        pdbx_PDB_model_num: CifField.ofColumn(Column.ofConst('1', atoms.count, Column.Schema.str)),
+    }
+
+    return {
+        entity: entityBuilder.getEntityCategory(),
+        chem_comp: componentBuilder.getChemCompCategory(),
+        atom_site: CifCategory.ofFields('atom_site', atom_site)
+    }
+}
+
+function psfToMmCif(psf: PsfFile) {
+    const categories = getCategories(psf.atoms)
+
+    return {
+        header: psf.id,
+        categoryNames: Object.keys(categories),
+        categories
+    };
+}
+
+export function topologyFromPsf(psf: PsfFile): Task<Topology> {
+    return Task.create('Parse PSF', async ctx => {
+        const label = psf.id
+        const cif = psfToMmCif(psf);
+        const format = ModelFormat.mmCIF(cif);
+
+        const { atomIdA, atomIdB } = psf.bonds
+
+        const bonds = {
+            indexA: Column.ofLambda({
+                value: (row: number) => atomIdA.value(row) - 1,
+                rowCount: atomIdA.rowCount,
+                schema: atomIdA.schema,
+            }),
+            indexB: Column.ofLambda({
+                value: (row: number) => atomIdB.value(row) - 1,
+                rowCount: atomIdB.rowCount,
+                schema: atomIdB.schema,
+            }),
+            order: Column.ofConst(1, psf.bonds.count, Column.Schema.int)
+        }
+
+        return Topology.create(label, format, bonds)
+    })
+}

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

@@ -1,10 +1,12 @@
 /**
- * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 export * from './structure/coordinates'
+export * from './structure/topology'
 export * from './structure/model'
 export * from './structure/structure'
 export * from './structure/query'

+ 9 - 32
src/mol-model/structure/coordinates/coordinates.ts

@@ -6,10 +6,7 @@
 
 import { UUID } from '../../../mol-util';
 import { Cell } from '../../../mol-math/geometry/spacegroup/cell';
-import { Model } from '../model';
 import { AtomicConformation } from '../model/properties/atomic';
-import { CustomProperties } from '../../structure';
-import { Mutable } from '../../../mol-util/type-helpers';
 import { Column } from '../../../mol-data/db';
 
 export interface Frame {
@@ -91,37 +88,17 @@ namespace Coordinates {
             timeOffset
         }
     }
-}
 
-function getAtomicConformation(frame: Frame, atomId: Column<number>): AtomicConformation {
-    return {
-        id: UUID.create22(),
-        atomId,
-        occupancy: Column.ofConst(1, frame.elementCount, Column.Schema.int),
-        B_iso_or_equiv: Column.ofConst(0, frame.elementCount, Column.Schema.float),
-        x: frame.x,
-        y: frame.y,
-        z: frame.z,
-    }
-}
-
-export function trajectoryFromModelAndCoordinates(model: Model, coordinates: Coordinates): Model.Trajectory {
-    const trajectory: Mutable<Model.Trajectory> = []
-    const { frames } = coordinates
-    for (let i = 0, il = frames.length; i < il; ++i) {
-        const f = frames[i]
-        const m = {
-            ...model,
+    export function getAtomicConformation(frame: Frame, atomId: Column<number>): AtomicConformation {
+        return {
             id: UUID.create22(),
-            modelNum: i,
-            atomicConformation: getAtomicConformation(f, model.atomicConformation.atomId),
-            // TODO: add support for supplying sphere and gaussian coordinates in addition to atomic coordinates
-            // coarseConformation: coarse.conformation,
-            customProperties: new CustomProperties(),
-            _staticPropertyData: Object.create(null),
-            _dynamicPropertyData: Object.create(null)
+            atomId,
+            occupancy: Column.ofConst(1, frame.elementCount, Column.Schema.int),
+            B_iso_or_equiv: Column.ofConst(0, frame.elementCount, Column.Schema.float),
+            xyzDefined: true,
+            x: frame.x,
+            y: frame.y,
+            z: frame.z,
         }
-        trajectory.push(m)
     }
-    return trajectory
 }

+ 40 - 0
src/mol-model/structure/model/model.ts

@@ -17,6 +17,12 @@ import { SaccharideComponentMap } from '../structure/carbohydrates/constants';
 import { ModelFormat } from '../../../mol-model-formats/structure/format';
 import { calcModelCenter } 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 { _parse_mmCif } from '../../../mol-model-formats/structure/mmcif/parser';
+import { Task } from '../../../mol-task';
+import { IndexPairBonds } from '../../../mol-model-formats/structure/mmcif/bonds/index-pair';
 
 /**
  * Interface to the "source data" of the molecule.
@@ -84,6 +90,40 @@ export namespace Model {
     // TODO: is this enough?
     export type Trajectory = ReadonlyArray<Model>
 
+    export function trajectoryFromModelAndCoordinates(model: Model, coordinates: Coordinates): Trajectory {
+        const trajectory: Mutable<Model.Trajectory> = []
+        const { frames } = coordinates
+        for (let i = 0, il = frames.length; i < il; ++i) {
+            const f = frames[i]
+            const m = {
+                ...model,
+                id: UUID.create22(),
+                modelNum: i,
+                atomicConformation: Coordinates.getAtomicConformation(f, model.atomicConformation.atomId),
+                // TODO: add support for supplying sphere and gaussian coordinates in addition to atomic coordinates?
+                // coarseConformation: coarse.conformation,
+                customProperties: new CustomProperties(),
+                _staticPropertyData: Object.create(null),
+                _dynamicPropertyData: Object.create(null)
+            }
+            trajectory.push(m)
+        }
+        return trajectory
+    }
+
+    export function trajectoryFromTopologyAndCoordinates(topology: Topology, coordinates: Coordinates): Task<Trajectory> {
+        return Task.create('Create Trajectory', async ctx => {
+            const model = (await _parse_mmCif(topology.format, ctx))[0];
+            if (!model) throw new Error('found no model')
+            const trajectory = trajectoryFromModelAndCoordinates(model, coordinates)
+            const bondData = { pairs: topology.bonds, count: model.atomicHierarchy.atoms._rowCount }
+            for (const m of trajectory) {
+                IndexPairBonds.attachFromData(m, bondData)
+            }
+            return trajectory
+        })
+    }
+
     const CenterProp = '__Center__'
     export function getCenter(model: Model): Vec3 {
         if (model._dynamicPropertyData[CenterProp]) return model._dynamicPropertyData[CenterProp]

+ 7 - 0
src/mol-model/structure/topology.ts

@@ -0,0 +1,7 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+export * from './topology/topology'

+ 58 - 0
src/mol-model/structure/topology/topology.ts

@@ -0,0 +1,58 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { UUID } from '../../../mol-util';
+import { Column } from '../../../mol-data/db';
+import { ModelFormat } from '../../../mol-model-formats/structure/format';
+
+export { Topology }
+
+interface Topology {
+    readonly id: UUID
+    readonly label: string
+
+    readonly format: ModelFormat
+
+    readonly bonds: {
+        readonly indexA: Column<number>,
+        readonly indexB: Column<number>
+        readonly order: Column<number>
+    }
+
+    // TODO
+    // readonly angles: {
+    //     readonly indexA: Column<number>
+    //     readonly indexB: Column<number>
+    //     readonly indexC: Column<number>
+    // }
+
+    // readonly dihedrals: {
+    //     readonly indexA: Column<number>
+    //     readonly indexB: Column<number>
+    //     readonly indexC: Column<number>
+    //     readonly indexD: Column<number>
+    // }
+
+    // readonly impropers: {
+    //     readonly indexA: Column<number>
+    //     readonly indexB: Column<number>
+    //     readonly indexC: Column<number>
+    //     readonly indexD: Column<number>
+    // }
+
+    // TODO: add forces for bonds/angles/dihedrals/impropers
+}
+
+namespace Topology {
+    export function create(label: string, format: ModelFormat, bonds: Topology['bonds']): Topology {
+        return {
+            id: UUID.create22(),
+            label,
+            format,
+            bonds
+        }
+    }
+}

+ 1 - 1
src/mol-plugin/index.ts

@@ -22,7 +22,7 @@ import { VolumeStreamingCustomControls } from '../mol-plugin-ui/custom/volume';
 export const DefaultPluginSpec: PluginSpec = {
     actions: [
         PluginSpec.Action(StateActions.Structure.DownloadStructure),
-        PluginSpec.Action(StateActions.Structure.AddTrajectoryFromModelAndCoordinates),
+        PluginSpec.Action(StateActions.Structure.AddTrajectory),
         PluginSpec.Action(StateActions.Volume.DownloadDensity),
         PluginSpec.Action(StateActions.DataFormat.OpenFile),
         PluginSpec.Action(StateActions.Structure.Create3DRepresentationPreset),

+ 11 - 8
src/mol-plugin/state/actions/structure.ts

@@ -11,7 +11,7 @@ import { ParamDefinition as PD } from '../../../mol-util/param-definition';
 import { PluginStateObject } from '../objects';
 import { StateTransforms } from '../transforms';
 import { Download, ParsePsf } from '../transforms/data';
-import { CustomModelProperties, StructureSelectionFromExpression, CustomStructureProperties, CoordinatesFromDcd, TrajectoryFromModelAndCoordinates } from '../transforms/model';
+import { CustomModelProperties, StructureSelectionFromExpression, CustomStructureProperties, CoordinatesFromDcd, TrajectoryFromModelAndCoordinates, TopologyFromPsf } from '../transforms/model';
 import { DataFormatProvider, guessCifVariant, DataFormatBuilderOptions } from './data-format';
 import { FileInfo } from '../../../mol-util/file-info';
 import { Task } from '../../../mol-task';
@@ -96,7 +96,7 @@ export const PsfProvider: DataFormatProvider<any> = {
     },
     getDefaultBuilder: (ctx: PluginContext, data: StateBuilder.To<PluginStateObject.Data.String>, options: DataFormatBuilderOptions, state: State) => {
         return Task.create('PSF default builder', async taskCtx => {
-            await state.updateTree(data.apply(ParsePsf)).runInContext(taskCtx)
+            await state.updateTree(data.apply(ParsePsf, {}, { state: { isGhost: true } }).apply(TopologyFromPsf)).runInContext(taskCtx)
         })
     }
 }
@@ -427,18 +427,21 @@ export const StructureFromSelection = StateAction.build({
     return state.updateTree(root);
 });
 
-export const AddTrajectoryFromModelAndCoordinates = StateAction.build({
-    display: { name: 'Add Trajectory', description: 'Add trajectory from existing model and coordinates.' },
+export const AddTrajectory = StateAction.build({
+    display: { name: 'Add Trajectory', description: 'Add trajectory from existing model/topology and coordinates.' },
     from: PluginStateObject.Root,
     params(a, ctx: PluginContext) {
         const state = ctx.state.dataState
-        const models = state.selectQ(q => q.rootsOfType(PluginStateObject.Molecule.Model))
-        const modelOptions = models.map(m => [m.transform.ref, m.obj!.label]) as [string, string][]
+        const models = [
+            ...state.selectQ(q => q.rootsOfType(PluginStateObject.Molecule.Model)),
+            ...state.selectQ(q => q.rootsOfType(PluginStateObject.Molecule.Topology)),
+        ]
+        const modelOptions = models.map(t => [t.transform.ref, t.obj!.label]) as [string, string][]
         const coords = state.selectQ(q => q.rootsOfType(PluginStateObject.Molecule.Coordinates))
-        const coordsOptions = coords.map(c => [c.transform.ref, c.obj!.label]) as [string, string][]
+        const coordOptions = coords.map(c => [c.transform.ref, c.obj!.label]) as [string, string][]
         return {
             model: PD.Select(modelOptions.length ? modelOptions[0][0] : '', modelOptions),
-            coordinates: PD.Select(coordsOptions.length ? coordsOptions[0][0] : '', coordsOptions)
+            coordinates: PD.Select(coordOptions.length ? coordOptions[0][0] : '', coordOptions)
         }
     }
 })(({ ref, params, state }, ctx: PluginContext) => {

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

@@ -8,6 +8,7 @@
 import { CifFile } from '../../mol-io/reader/cif';
 import { PlyFile } from '../../mol-io/reader/ply/schema';
 import { Coordinates as _Coordinates } from '../../mol-model/structure';
+import { Topology as _Topology } from '../../mol-model/structure';
 import { Model as _Model, Structure as _Structure, StructureElement } from '../../mol-model/structure';
 import { VolumeData } from '../../mol-model/volume';
 import { PluginBehavior } from '../../mol-plugin/behavior/behavior';
@@ -97,6 +98,7 @@ export namespace PluginStateObject {
 
     export namespace Molecule {
         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 Structure extends Create<_Structure>({ name: 'Structure', typeClass: 'Object' }) { }

+ 33 - 6
src/mol-plugin/state/transforms/model.ts

@@ -9,7 +9,7 @@ import { parsePDB } from '../../../mol-io/reader/pdb/parser';
 import { Vec3, Mat4, Quat } from '../../../mol-math/linear-algebra';
 import { trajectoryFromMmCIF } from '../../../mol-model-formats/structure/mmcif';
 import { trajectoryFromPDB } from '../../../mol-model-formats/structure/pdb';
-import { Model, Queries, QueryContext, Structure, StructureQuery, StructureSelection as Sel, StructureElement, trajectoryFromModelAndCoordinates, Coordinates } from '../../../mol-model/structure';
+import { Model, Queries, QueryContext, Structure, StructureQuery, StructureSelection as Sel, StructureElement, Coordinates, Topology } from '../../../mol-model/structure';
 import { PluginContext } from '../../../mol-plugin/context';
 import { MolScriptBuilder } from '../../../mol-script/language/builder';
 import Expression from '../../../mol-script/language/expression';
@@ -30,8 +30,10 @@ import { StructureQueryHelper } from '../../util/structure-query';
 import { ModelStructureRepresentation } from '../representation/model';
 import { parseDcd } from '../../../mol-io/reader/dcd/parser';
 import { coordinatesFromDcd } from '../../../mol-model-formats/structure/dcd';
+import { topologyFromPsf } from '../../../mol-model-formats/structure/psf';
 
 export { CoordinatesFromDcd };
+export { TopologyFromPsf };
 export { TrajectoryFromModelAndCoordinates };
 export { TrajectoryFromBlob };
 export { TrajectoryFromMmCif };
@@ -64,15 +66,41 @@ const CoordinatesFromDcd = PluginStateTransform.BuiltIn({
             const parsed = await parseDcd(a.data).runInContext(ctx);
             if (parsed.isError) throw new Error(parsed.message);
             const coordinates = await coordinatesFromDcd(parsed.result).runInContext(ctx);
-            return new SO.Molecule.Coordinates(coordinates, { label: a.label, description: a.description });
+            return new SO.Molecule.Coordinates(coordinates, { label: a.label, description: 'Coordinates' });
         });
     }
 });
 
+type TopologyFromPsf = typeof TopologyFromPsf
+const TopologyFromPsf = PluginStateTransform.BuiltIn({
+    name: 'topology-from-psf',
+    display: { name: 'PSF Topology', description: 'Parse PSF string data.' },
+    from: [SO.Format.Psf],
+    to: SO.Molecule.Topology
+})({
+    apply({ a }) {
+        return Task.create('Create Topology', async ctx => {
+            const topology = await topologyFromPsf(a.data).runInContext(ctx);
+            return new SO.Molecule.Topology(topology, { label: topology.label || a.label, description: 'Topology' });
+        });
+    }
+});
+
+async function getTrajectory(ctx: RuntimeContext, obj: StateObject, coordinates: Coordinates) {
+    if (obj.type === SO.Molecule.Topology.type) {
+        const topology = obj.data as Topology
+        return await Model.trajectoryFromTopologyAndCoordinates(topology, coordinates).runInContext(ctx);
+    } else if (obj.type === SO.Molecule.Model.type) {
+        const model = obj.data as Model
+        return Model.trajectoryFromModelAndCoordinates(model, coordinates);
+    }
+    throw new Error('no model/topology found')
+}
+
 type TrajectoryFromModelAndCoordinates = typeof TrajectoryFromModelAndCoordinates
 const TrajectoryFromModelAndCoordinates = PluginStateTransform.BuiltIn({
     name: 'trajectory-from-model-and-coordinates',
-    display: { name: 'Trajectory from Model & Coordinates', description: 'Create a trajectory from existing model and coordinates.' },
+    display: { name: 'Trajectory from Topology & Coordinates', description: 'Create a trajectory from existing model/topology and coordinates.' },
     from: SO.Root,
     to: SO.Molecule.Trajectory,
     params: {
@@ -81,10 +109,9 @@ const TrajectoryFromModelAndCoordinates = PluginStateTransform.BuiltIn({
     }
 })({
     apply({ params, dependencies }) {
-        return Task.create('Create trajectory from model and coordinates', async ctx => {
-            const model = dependencies![params.modelRef].data as Model
+        return Task.create('Create trajectory from model/topology and coordinates', async ctx => {
             const coordinates = dependencies![params.coordinatesRef].data as Coordinates
-            const trajectory = trajectoryFromModelAndCoordinates(model, coordinates);
+            const trajectory = await getTrajectory(ctx, dependencies![params.modelRef], coordinates);
             const props = { label: 'Trajectory', description: `${trajectory.length} model${trajectory.length === 1 ? '' : 's'}` };
             return new SO.Molecule.Trajectory(trajectory, props);
         });