Parcourir la source

add top format support

Alexander Rose il y a 3 ans
Parent
commit
b3b4692237

+ 1 - 1
CHANGELOG.md

@@ -14,7 +14,7 @@ Note that since we don't clearly distinguish between a public and private interf
 - Fix wrong element assignment for atoms with Charmm ion names
 - Fix handling of empty symmetry cell data
 - Add support for ``trr`` and ``nctraj`` coordinates files
-- Add support for ``prmtop`` topology files
+- Add support for ``prmtop`` and ``top`` topology files
 
 ## [v3.3.1] - 2022-02-27
 

+ 303 - 0
src/mol-io/reader/top/parser.ts

@@ -0,0 +1,303 @@
+/**
+ * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Task, RuntimeContext } from '../../../mol-task';
+import { Tokenizer, TokenBuilder } from '../common/text/tokenizer';
+import { ReaderResult as Result } from '../result';
+import { TokenColumnProvider as TokenColumn } from '../common/text/column/token';
+import { Column, Table } from '../../../mol-data/db';
+import { Mutable } from '../../../mol-util/type-helpers';
+
+// https://manual.gromacs.org/2021-current/reference-manual/file-formats.html#top
+
+const AtomsSchema = {
+    nr: Column.Schema.Int(),
+    type: Column.Schema.Str(),
+    resnr: Column.Schema.Int(),
+    residu: Column.Schema.Str(),
+    atom: Column.Schema.Str(),
+    cgnr: Column.Schema.Int(),
+    charge: Column.Schema.Float(),
+    mass: Column.Schema.Float(),
+};
+
+const BondsSchema = {
+    ai: Column.Schema.Int(),
+    aj: Column.Schema.Int(),
+};
+
+const MoleculesSchema = {
+    compound: Column.Schema.Str(),
+    molCount: Column.Schema.Int(),
+};
+
+type Compound = {
+    atoms: Table<typeof AtomsSchema>
+    bonds?: Table<typeof BondsSchema>
+}
+
+export interface TopFile {
+    readonly system: string
+    readonly molecules: Table<typeof MoleculesSchema>
+    readonly compounds: Record<string, Compound>
+}
+
+const { readLine, markLine, skipWhitespace, markStart, eatValue, eatLine } = Tokenizer;
+
+function State(tokenizer: Tokenizer, runtimeCtx: RuntimeContext) {
+    return {
+        tokenizer,
+        runtimeCtx,
+    };
+}
+type State = ReturnType<typeof State>
+
+const reField = /\[ (.+) \]/;
+const reWhitespace = /\s+/;
+
+function handleMoleculetype(state: State) {
+    const { tokenizer } = state;
+
+    let molName: string | undefined = undefined;
+
+    while (tokenizer.tokenEnd < tokenizer.length) {
+        skipWhitespace(tokenizer);
+        const c = tokenizer.data[tokenizer.position];
+        if (c === '[') break;
+        if (c === ';' || c === '*') {
+            markLine(tokenizer);
+            continue;
+        }
+
+        if (molName !== undefined) throw new Error('more than one molName');
+
+        const line = readLine(tokenizer);
+        molName = line.split(reWhitespace)[0];
+    }
+
+    if (molName === undefined) throw new Error('missing molName');
+
+    return molName;
+}
+
+function handleAtoms(state: State) {
+    const { tokenizer } = state;
+
+    const nr = TokenBuilder.create(tokenizer.data, 64);
+    const type = TokenBuilder.create(tokenizer.data, 64);
+    const resnr = TokenBuilder.create(tokenizer.data, 64);
+    const residu = TokenBuilder.create(tokenizer.data, 64);
+    const atom = TokenBuilder.create(tokenizer.data, 64);
+    const cgnr = TokenBuilder.create(tokenizer.data, 64);
+    const charge = TokenBuilder.create(tokenizer.data, 64);
+    const mass = TokenBuilder.create(tokenizer.data, 64);
+
+    while (tokenizer.tokenEnd < tokenizer.length) {
+        skipWhitespace(tokenizer);
+        const c = tokenizer.data[tokenizer.position];
+        if (c === '[') break;
+        if (c === ';' || c === '*') {
+            markLine(tokenizer);
+            continue;
+        }
+
+        for (let j = 0; j < 8; ++j) {
+            skipWhitespace(tokenizer);
+            markStart(tokenizer);
+            eatValue(tokenizer);
+
+            switch (j) {
+                case 0: TokenBuilder.add(nr, tokenizer.tokenStart, tokenizer.tokenEnd); break;
+                case 1: TokenBuilder.add(type, tokenizer.tokenStart, tokenizer.tokenEnd); break;
+                case 2: TokenBuilder.add(resnr, tokenizer.tokenStart, tokenizer.tokenEnd); break;
+                case 3: TokenBuilder.add(residu, tokenizer.tokenStart, tokenizer.tokenEnd); break;
+                case 4: TokenBuilder.add(atom, tokenizer.tokenStart, tokenizer.tokenEnd); break;
+                case 5: TokenBuilder.add(cgnr, tokenizer.tokenStart, tokenizer.tokenEnd); break;
+                case 6: TokenBuilder.add(charge, tokenizer.tokenStart, tokenizer.tokenEnd); break;
+                case 7: TokenBuilder.add(mass, tokenizer.tokenStart, tokenizer.tokenEnd); break;
+            }
+        }
+        // ignore any extra columns
+        markLine(tokenizer);
+    }
+
+    return Table.ofColumns(AtomsSchema, {
+        nr: TokenColumn(nr)(Column.Schema.int),
+        type: TokenColumn(type)(Column.Schema.str),
+        resnr: TokenColumn(resnr)(Column.Schema.int),
+        residu: TokenColumn(residu)(Column.Schema.str),
+        atom: TokenColumn(atom)(Column.Schema.str),
+        cgnr: TokenColumn(cgnr)(Column.Schema.int),
+        charge: TokenColumn(charge)(Column.Schema.float),
+        mass: TokenColumn(mass)(Column.Schema.float),
+    });
+}
+
+function handleBonds(state: State) {
+    const { tokenizer } = state;
+
+    const ai = TokenBuilder.create(tokenizer.data, 64);
+    const aj = TokenBuilder.create(tokenizer.data, 64);
+
+    while (tokenizer.tokenEnd < tokenizer.length) {
+        skipWhitespace(tokenizer);
+        const c = tokenizer.data[tokenizer.position];
+        if (c === '[') break;
+        if (c === ';' || c === '*') {
+            markLine(tokenizer);
+            continue;
+        }
+
+        for (let j = 0; j < 2; ++j) {
+            skipWhitespace(tokenizer);
+            markStart(tokenizer);
+            eatValue(tokenizer);
+
+            switch (j) {
+                case 0: TokenBuilder.add(ai, tokenizer.tokenStart, tokenizer.tokenEnd); break;
+                case 1: TokenBuilder.add(aj, tokenizer.tokenStart, tokenizer.tokenEnd); break;
+            }
+        }
+        // ignore any extra columns
+        markLine(tokenizer);
+    }
+
+    return Table.ofColumns(BondsSchema, {
+        ai: TokenColumn(ai)(Column.Schema.int),
+        aj: TokenColumn(aj)(Column.Schema.int),
+    });
+}
+
+function handleSystem(state: State) {
+    const { tokenizer } = state;
+
+    let system: string | undefined = undefined;
+
+    while (tokenizer.tokenEnd < tokenizer.length) {
+        skipWhitespace(tokenizer);
+        const c = tokenizer.data[tokenizer.position];
+        if (c === '[') break;
+        if (c === ';' || c === '*') {
+            markLine(tokenizer);
+            continue;
+        }
+
+        if (system !== undefined) throw new Error('more than one system');
+        system = readLine(tokenizer).trim();
+    }
+
+    if (system === undefined) throw new Error('missing system');
+
+    return system;
+}
+
+function handleMolecules(state: State) {
+    const { tokenizer } = state;
+
+    const compound = TokenBuilder.create(tokenizer.data, 64);
+    const molCount = TokenBuilder.create(tokenizer.data, 64);
+
+    while (tokenizer.tokenEnd < tokenizer.length) {
+        skipWhitespace(tokenizer);
+        if (tokenizer.position >= tokenizer.length) break;
+
+        const c = tokenizer.data[tokenizer.position];
+        if (c === '[') break;
+        if (c === ';' || c === '*') {
+            markLine(tokenizer);
+            continue;
+        }
+
+        for (let j = 0; j < 2; ++j) {
+            skipWhitespace(tokenizer);
+            markStart(tokenizer);
+            eatValue(tokenizer);
+
+            switch (j) {
+                case 0: TokenBuilder.add(compound, tokenizer.tokenStart, tokenizer.tokenEnd); break;
+                case 1: TokenBuilder.add(molCount, tokenizer.tokenStart, tokenizer.tokenEnd); break;
+            }
+        }
+        // ignore any extra columns
+        eatLine(tokenizer);
+        markStart(tokenizer);
+    }
+
+    return Table.ofColumns(MoleculesSchema, {
+        compound: TokenColumn(compound)(Column.Schema.str),
+        molCount: TokenColumn(molCount)(Column.Schema.int),
+    });
+}
+
+async function parseInternal(data: string, ctx: RuntimeContext): Promise<Result<TopFile>> {
+    const t = Tokenizer(data);
+    const state = State(t, ctx);
+
+    const result: Mutable<TopFile> = Object.create(null);
+    let prevPosition = 0;
+
+    result.compounds = {};
+    let currentCompound: Partial<Compound> = {};
+    let currentMolName = '';
+
+    function addMol() {
+        if (currentMolName && currentCompound.atoms) {
+            result.compounds[currentMolName] = currentCompound as Compound;
+            currentCompound = {};
+            currentMolName = '';
+        }
+    }
+
+    while (t.tokenEnd < t.length) {
+        if (t.position - prevPosition > 100000 && ctx.shouldUpdate) {
+            prevPosition = t.position;
+            await ctx.update({ current: t.position, max: t.length });
+        }
+
+        const line = readLine(state.tokenizer).trim();
+
+        if (!line || line[0] === '*' || line[0] === ';') {
+            continue;
+        }
+
+        if (line.startsWith('#include')) {
+            throw new Error('#include statements not allowed');
+        }
+
+        if (line.startsWith('[')) {
+            const fieldMatch = line.match(reField);
+            if (fieldMatch === null) throw new Error('expected field name');
+
+            const fieldName = fieldMatch[1];
+            if (fieldName === 'moleculetype') {
+                addMol();
+                currentMolName = handleMoleculetype(state);
+            } else if (fieldName === 'atoms') {
+                currentCompound.atoms = handleAtoms(state);
+            } else if (fieldName === 'bonds') {
+                currentCompound.bonds = handleBonds(state);
+            } else if (fieldName === 'system') {
+                result.system = handleSystem(state);
+            } else if (fieldName === 'molecules') {
+                addMol(); // add the last compound
+                result.molecules = handleMolecules(state);
+            } else {
+                while (t.tokenEnd < t.length) {
+                    if (t.data[t.position] === '[') break;
+                    markLine(t);
+                }
+            }
+        }
+    }
+
+    return Result.success(result);
+}
+
+export function parseTop(data: string) {
+    return Task.create<Result<TopFile>>('Parse TOP', async ctx => {
+        return await parseInternal(data, ctx);
+    });
+}

+ 226 - 0
src/mol-model-formats/structure/top.ts

@@ -0,0 +1,226 @@
+/**
+ * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Column, Table } from '../../mol-data/db';
+import { TopFile } from '../../mol-io/reader/top/parser';
+import { getMoleculeType, MoleculeType } from '../../mol-model/structure/model/types';
+import { Topology } from '../../mol-model/structure/topology/topology';
+import { Task } from '../../mol-task';
+import { ModelFormat } from '../format';
+import { BasicSchema, createBasic } from './basic/schema';
+import { ComponentBuilder } from './common/component';
+import { EntityBuilder } from './common/entity';
+import { getChainId } from './common/util';
+import { guessElementSymbolString } from './util';
+
+function getBasic(top: TopFile) {
+    const { molecules, compounds } = top;
+
+    const singleResidue: Record<string, boolean> = {};
+    let atomCount = 0;
+
+    for (let i = 0, il = molecules._rowCount; i < il; ++i) {
+        const mol = molecules.compound.value(i);
+        const count = molecules.molCount.value(i);
+        const { atoms } = compounds[mol];
+
+        Column.asArrayColumn(atoms.atom);
+        Column.asArrayColumn(atoms.resnr);
+        Column.asArrayColumn(atoms.residu);
+
+        atomCount += count * atoms._rowCount;
+
+        let prevResnr = atoms.resnr.value(0);
+        singleResidue[mol] = true;
+        for (let j = 1, jl = atoms._rowCount; j < jl; ++j) {
+            const resnr = atoms.resnr.value(j);
+            if (resnr !== prevResnr) {
+                singleResidue[mol] = false;
+                break;
+            }
+            prevResnr = resnr;
+        }
+    }
+
+    //
+
+    const atomNames = new Array<string>(atomCount);
+    const residueIds = new Uint32Array(atomCount);
+    const residueNames = new Array<string>(atomCount);
+
+    let k = 0;
+    for (let i = 0, il = molecules._rowCount; i < il; ++i) {
+        const mol = molecules.compound.value(i);
+        const count = molecules.molCount.value(i);
+        const { atoms } = compounds[mol];
+        const isSingleResidue = singleResidue[mol];
+        for (let j = 0; j < count; ++j) {
+            for (let l = 0, ll = atoms._rowCount; l < ll; ++l) {
+                atomNames[k] = atoms.atom.value(l);
+                residueIds[k] = atoms.resnr.value(l);
+                residueNames[k] = atoms.residu.value(l);
+
+                if (isSingleResidue) residueIds[k] += j;
+
+                k += 1;
+            }
+        }
+    }
+
+    const atomName = Column.ofStringArray(atomNames);
+    const residueId = Column.ofIntArray(residueIds);
+    const residueName = Column.ofStringArray(residueNames);
+
+    //
+
+    const entityIds = new Array<string>(atomCount);
+    const asymIds = new Array<string>(atomCount);
+    const seqIds = new Uint32Array(atomCount);
+    const ids = new Uint32Array(atomCount);
+
+    const entityBuilder = new EntityBuilder();
+    const componentBuilder = new ComponentBuilder(residueId, atomName);
+
+    let currentEntityId = '';
+    let currentAsymIndex = 0;
+    let currentAsymId = '';
+    let currentSeqId = 0;
+    let prevMoleculeType = MoleculeType.Unknown;
+    let prevResidueNumber = -1;
+
+    for (let i = 0, il = atomCount; i < il; ++i) {
+        const residueNumber = residueId.value(i);
+        if (residueNumber !== prevResidueNumber) {
+            const compId = residueName.value(i);
+            const moleculeType = getMoleculeType(componentBuilder.add(compId, i).type, compId);
+
+            if (moleculeType !== prevMoleculeType) {
+                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 id = Column.ofIntArray(ids);
+    const asym_id = Column.ofStringArray(asymIds);
+
+    //
+
+    const type_symbol = new Array<string>(atomCount);
+    for (let i = 0; i < atomCount; ++i) {
+        type_symbol[i] = guessElementSymbolString(atomName.value(i), residueName.value(i));
+    }
+
+    const atom_site = Table.ofPartialColumns(BasicSchema.atom_site, {
+        auth_asym_id: asym_id,
+        auth_atom_id: Column.asArrayColumn(atomName),
+        auth_comp_id: residueName,
+        auth_seq_id: residueId,
+        id: Column.asArrayColumn(id),
+
+        label_asym_id: asym_id,
+        label_atom_id: Column.asArrayColumn(atomName),
+        label_comp_id: residueName,
+        label_seq_id: Column.ofIntArray(seqIds),
+        label_entity_id: Column.ofStringArray(entityIds),
+
+        occupancy: Column.ofConst(1, atomCount, Column.Schema.float),
+        type_symbol: Column.ofStringArray(type_symbol),
+
+        pdbx_PDB_model_num: Column.ofConst(1, atomCount, Column.Schema.int),
+    }, atomCount);
+
+    const basic = createBasic({
+        entity: entityBuilder.getEntityTable(),
+        chem_comp: componentBuilder.getChemCompTable(),
+        atom_site
+    });
+
+    return basic;
+}
+
+function getBonds(top: TopFile) {
+    const { molecules, compounds } = top;
+
+    const indexA: number[] = [];
+    const indexB: number[] = [];
+
+    let atomOffset = 0;
+
+    for (let i = 0, il = molecules._rowCount; i < il; ++i) {
+        const mol = molecules.compound.value(i);
+        const count = molecules.molCount.value(i);
+        const { atoms, bonds } = compounds[mol];
+
+
+
+        if (bonds) {
+            for (let j = 0; j < count; ++j) {
+
+                for (let l = 0, ll = bonds._rowCount; l < ll; ++l) {
+                    indexA.push(bonds.ai.value(l) - 1 + atomOffset);
+                    indexB.push(bonds.aj.value(l) - 1 + atomOffset);
+                }
+
+                atomOffset += atoms._rowCount;
+            }
+        } else if (mol === 'TIP3') {
+            for (let j = 0; j < count; ++j) {
+                indexA.push(0 + atomOffset);
+                indexB.push(1 + atomOffset);
+                indexA.push(0 + atomOffset);
+                indexB.push(2 + atomOffset);
+                atomOffset += atoms._rowCount;
+            }
+        } else {
+            atomOffset += count * atoms._rowCount;
+        }
+    }
+
+    return {
+        indexA: Column.ofIntArray(indexA),
+        indexB: Column.ofIntArray(indexB),
+        order: Column.ofConst(1, indexA.length, Column.Schema.int)
+    };
+}
+
+//
+
+export { TopFormat };
+
+type TopFormat = ModelFormat<TopFile>
+
+namespace TopFormat {
+    export function is(x?: ModelFormat): x is TopFormat {
+        return x?.kind === 'top';
+    }
+
+    export function fromTop(top: TopFile): TopFormat {
+        return { kind: 'top', name: top.system || 'TOP', data: top };
+    }
+}
+
+export function topologyFromTop(top: TopFile): Task<Topology> {
+    return Task.create('Parse TOP', async ctx => {
+        const format = TopFormat.fromTop(top);
+        const basic = getBasic(top);
+        const bonds = getBonds(top);
+
+        return Topology.create(top.system || 'TOP', basic, bonds, format);
+    });
+}

+ 20 - 0
src/mol-plugin-state/formats/topology.ts

@@ -48,11 +48,31 @@ const PrmtopProvider = DataFormatProvider({
 });
 type PrmtopProvider = typeof PrmtopProvider;
 
+export { TopProvider };
+const TopProvider = DataFormatProvider({
+    label: 'TOP',
+    description: 'TOP',
+    category: TopologyFormatCategory,
+    stringExtensions: ['top'],
+    parse: async (plugin, data) => {
+        const format = plugin.state.data.build()
+            .to(data)
+            .apply(StateTransforms.Data.ParseTop, {}, { state: { isGhost: true } });
+        const topology = format.apply(StateTransforms.Model.TopologyFromTop);
+
+        await format.commit();
+
+        return { format: format.selector, topology: topology.selector };
+    }
+});
+type TopProvider = typeof TopProvider;
+
 export type TopologyProvider = PsfProvider;
 
 export const BuiltInTopologyFormats = [
     ['psf', PsfProvider] as const,
     ['prmtop', PrmtopProvider] as const,
+    ['top', TopProvider] as const,
 ] as const;
 
 export type BuiltInTopologyFormat = (typeof BuiltInTopologyFormats)[number][0]

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

@@ -25,6 +25,7 @@ import { DxFile } from '../mol-io/reader/dx/parser';
 import { Color } from '../mol-util/color/color';
 import { Asset } from '../mol-util/assets';
 import { PrmtopFile } from '../mol-io/reader/prmtop/parser';
+import { TopFile } from '../mol-io/reader/top/parser';
 
 export type TypeClass = 'root' | 'data' | 'prop'
 
@@ -74,6 +75,7 @@ export namespace PluginStateObject {
         export class Cube extends Create<CubeFile>({ name: 'Cube File', typeClass: 'Data' }) { }
         export class Psf extends Create<PsfFile>({ name: 'PSF File', typeClass: 'Data' }) { }
         export class Prmtop extends Create<PrmtopFile>({ name: 'PRMTOP File', typeClass: 'Data' }) { }
+        export class Top extends Create<TopFile>({ name: 'TOP 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' }) { }
         export class Dsn6 extends Create<Dsn6File>({ name: 'DSN6/BRIX File', typeClass: 'Data' }) { }

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

@@ -22,6 +22,7 @@ import { parseDx } from '../../mol-io/reader/dx/parser';
 import { ColorNames } from '../../mol-util/color/names';
 import { assertUnreachable } from '../../mol-util/type-helpers';
 import { parsePrmtop } from '../../mol-io/reader/prmtop/parser';
+import { parseTop } from '../../mol-io/reader/top/parser';
 
 export { Download };
 export { DownloadBlob };
@@ -32,6 +33,7 @@ export { ParseCif };
 export { ParseCube };
 export { ParsePsf };
 export { ParsePrmtop };
+export { ParseTop };
 export { ParsePly };
 export { ParseCcp4 };
 export { ParseDsn6 };
@@ -335,6 +337,22 @@ const ParsePrmtop = PluginStateTransform.BuiltIn({
     }
 });
 
+type ParseTop = typeof ParseTop
+const ParseTop = PluginStateTransform.BuiltIn({
+    name: 'parse-top',
+    display: { name: 'Parse TOP', description: 'Parse TOP from String data' },
+    from: [SO.Data.String],
+    to: SO.Format.Top
+})({
+    apply({ a }) {
+        return Task.create('Parse TOP', async ctx => {
+            const parsed = await parseTop(a.data).runInContext(ctx);
+            if (parsed.isError) throw new Error(parsed.message);
+            return new SO.Format.Top(parsed.result);
+        });
+    }
+});
+
 type ParsePly = typeof ParsePly
 const ParsePly = PluginStateTransform.BuiltIn({
     name: 'parse-ply',

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

@@ -47,6 +47,7 @@ import { coordinatesFromTrr } from '../../mol-model-formats/structure/trr';
 import { parseNctraj } from '../../mol-io/reader/nctraj/parser';
 import { coordinatesFromNctraj } from '../../mol-model-formats/structure/nctraj';
 import { topologyFromPrmtop } from '../../mol-model-formats/structure/prmtop';
+import { topologyFromTop } from '../../mol-model-formats/structure/top';
 
 export { CoordinatesFromDcd };
 export { CoordinatesFromXtc };
@@ -54,6 +55,7 @@ export { CoordinatesFromTrr };
 export { CoordinatesFromNctraj };
 export { TopologyFromPsf };
 export { TopologyFromPrmtop };
+export { TopologyFromTop };
 export { TrajectoryFromModelAndCoordinates };
 export { TrajectoryFromBlob };
 export { TrajectoryFromMmCif };
@@ -177,6 +179,21 @@ const TopologyFromPrmtop = PluginStateTransform.BuiltIn({
     }
 });
 
+type TopologyFromTop = typeof TopologyFromTop
+const TopologyFromTop = PluginStateTransform.BuiltIn({
+    name: 'topology-from-top',
+    display: { name: 'TOP Topology', description: 'Create topology from TOP.' },
+    from: [SO.Format.Top],
+    to: SO.Molecule.Topology
+})({
+    apply({ a }) {
+        return Task.create('Create Topology', async ctx => {
+            const topology = await topologyFromTop(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;