Ver Fonte

Basic IHM support

David Sehnal há 7 anos atrás
pai
commit
e3a350deeb

+ 90 - 9
src/apps/structure-info/index.ts

@@ -5,14 +5,38 @@
  */
 
 import * as argparse from 'argparse'
+import * as util from 'util'
+import * as fs from 'fs'
 import fetch from 'node-fetch'
 require('util.promisify').shim();
 
 // import { Table } from 'mol-data/db'
 import CIF from 'mol-io/reader/cif'
-import { Model, Structure, ElementSet, Unit, ElementGroup } from 'mol-model/structure'
+import { Model, Structure, Element, ElementSet, Unit, ElementGroup, Queries } from 'mol-model/structure'
 import { Run, Progress } from 'mol-task'
 import { OrderedSet } from 'mol-data/int';
+import { Table } from 'mol-data/db';
+import { mmCIF_Database } from 'mol-io/reader/cif/schema/mmcif';
+import CoarseGrained from 'mol-model/structure/model/properties/coarse-grained';
+
+const readFileAsync = util.promisify(fs.readFile);
+
+async function readFile(path: string) {
+    if (path.match(/\.bcif$/)) {
+        const input = await readFileAsync(path)
+        const data = new Uint8Array(input.byteLength);
+        for (let i = 0; i < input.byteLength; i++) data[i] = input[i];
+        return data;
+    } else {
+        return readFileAsync(path, 'utf8');
+    }
+}
+
+async function readCif(path: string) {
+    const data = await readFile(path);
+    const parsed = await parseCif(data);
+    return CIF.schema.mmCIF(parsed.result.blocks[0])
+}
 
 async function parseCif(data: string|Uint8Array) {
     const comp = CIF.parse(data);
@@ -68,6 +92,7 @@ export function printBonds(structure: Structure) {
 }
 
 export function printSequence(model: Model) {
+    console.log('Sequence\n=============');
     const { byEntityKey } = model.sequence;
     for (const key of Object.keys(byEntityKey)) {
         const seq = byEntityKey[+key];
@@ -76,27 +101,83 @@ export function printSequence(model: Model) {
         //     console.log(`${seq.entityId} ${seq.num.value(i)} ${seq.compId.value(i)}`);
         // }
     }
+    console.log();
 }
 
-async function run(pdb: string) {
-    const mmcif = await getPdb(pdb)
+export function printUnits(structure: Structure) {
+    console.log('Units\n=============');
+    const { elements, units } = structure;
+    const unitIds = ElementSet.unitIndices(elements);
+    const l = Element.Location();
+
+    for (let i = 0, _i = unitIds.length; i < _i; i++) {
+        const unitId = unitIds[i];
+        l.unit = units[unitId];
+        const set = ElementSet.groupAt(elements, i).elements;
+        const size = OrderedSet.size(set);
+
+        if (Unit.isAtomic(l.unit)) {
+            console.log(`Atomic unit ${unitId}: ${size} elements`);
+        } else if (Unit.isCoarse(l.unit)) {
+            console.log(`Coarse unit ${unitId} (${l.unit.elementType === CoarseGrained.ElementType.Sphere ? 'spheres' : 'gaussians'}): ${size} elements.`);
+
+            const props = Queries.props.coarse_grained;
+            const seq = l.unit.model.sequence;
+
+            for (let j = 0, _j = Math.min(size, 10); j < _j; j++) {
+                l.element = OrderedSet.getAt(set, j);
+
+                const residues: string[] = [];
+                const start = props.seq_id_begin(l), end = props.seq_id_end(l);
+                const compId = seq.byEntityKey[props.entityKey(l)].compId.value;
+                for (let e = start; e <= end; e++) residues.push(compId(e));
+                console.log(`${props.asym_id(l)}:${start}-${end} (${residues.join('-')}) ${props.asym_id(l)} [${props.x(l).toFixed(2)}, ${props.y(l).toFixed(2)}, ${props.z(l).toFixed(2)}]`);
+            }
+            if (size > 10) console.log(`...`);
+        }
+    }
+}
+
+
+export function printIHMModels(model: Model) {
+    if (!model.coarseGrained.isDefined) return false;
+    console.log('IHM Models\n=============');
+    console.log(Table.formatToString(model.coarseGrained.modelList));
+}
+
+async function run(mmcif: mmCIF_Database) {
     const models = Model.create({ kind: 'mmCIF', data: mmcif });
-    //const structure = Structure.ofModel(models[0])
-    // console.log(structure)
-    // printBonds(structure)
+    const structure = Structure.ofModel(models[0]);
     printSequence(models[0]);
+    printIHMModels(models[0]);
+    printUnits(structure);
+}
+
+async function runDL(pdb: string) {
+    const mmcif = await getPdb(pdb)
+    run(mmcif);
+}
+
+async function runFile(filename: string) {
+    const mmcif = await readCif(filename);
+    run(mmcif);
 }
 
 const parser = new argparse.ArgumentParser({
   addHelp: true,
   description: 'Print info about a structure, mainly to test and showcase the mol-model module'
 });
-parser.addArgument([ '--pdb', '-p' ], {
+parser.addArgument([ '--download', '-d' ], {
     help: 'Pdb entry id'
 });
+parser.addArgument([ '--file', '-f' ], {
+    help: 'filename'
+});
 interface Args {
-    pdb: string
+    download?: string,
+    file?: string
 }
 const args: Args = parser.parseArgs();
 
-run(args.pdb)
+if (args.download) runDL(args.download)
+else if (args.file) runFile(args.file)

+ 33 - 0
src/mol-data/db/table.ts

@@ -6,6 +6,7 @@
 
 import Column from './column'
 import { sortArray } from '../util/sort'
+import { StringBuilder } from 'mol-util';
 
 /** A collection of columns */
 type Table<Schema extends Table.Schema> = {
@@ -188,6 +189,38 @@ namespace Table {
         }
         return ret;
     }
+
+    export function formatToString<S extends Schema>(table: Table<S>) {
+        const sb = StringBuilder.create();
+
+        const { _columns: cols, _rowCount } = table;
+
+        let headerLength = 1;
+        StringBuilder.write(sb, '|');
+        for (let i = 0; i < cols.length; i++) {
+            StringBuilder.write(sb, cols[i]);
+            StringBuilder.write(sb, '|');
+            headerLength += cols[i].length + 1;
+        }
+        StringBuilder.newline(sb);
+        StringBuilder.write(sb, new Array(headerLength + 1).join('-'));
+        StringBuilder.newline(sb);
+
+        for (let r = 0; r < _rowCount; r++) {
+            StringBuilder.write(sb, '|');
+            for (let i = 0; i < cols.length; i++) {
+                const c = table[cols[i]];
+                if (c.valueKind(r) === Column.ValueKind.Present) {
+                    StringBuilder.write(sb, c.value(r));
+                    StringBuilder.write(sb, '|');
+                } else {
+                    StringBuilder.write(sb, '.|');
+                }
+            }
+            StringBuilder.newline(sb);
+        }
+        return StringBuilder.getString(sb);
+    }
 }
 
 export default Table

+ 11 - 4
src/mol-model/structure/model/formats/mmcif.ts

@@ -11,7 +11,6 @@ import Format from '../format'
 import Model from '../model'
 import * as Hierarchy from '../properties/hierarchy'
 import AtomSiteConformation from '../properties/atom-site-conformation'
-import CoarseGrained from '../properties/coarse-grained'
 import Symmetry from '../properties/symmetry'
 import findHierarchyKeys from '../utils/hierarchy-keys'
 import { ElementSymbol} from '../types'
@@ -20,6 +19,7 @@ import createAssemblies from './mmcif/assembly'
 import mmCIF_Format = Format.mmCIF
 import { getSequence } from './mmcif/sequence';
 import { Entities } from '../properties/common';
+import { coarseGrainedFromIHM } from './mmcif/ihm';
 
 function findModelBounds({ data }: mmCIF_Format, startIndex: number) {
     const num = data.atom_site.pdbx_PDB_model_num;
@@ -31,6 +31,8 @@ function findModelBounds({ data }: mmCIF_Format, startIndex: number) {
 }
 
 function findHierarchyOffsets({ data }: mmCIF_Format, bounds: Interval) {
+    if (Interval.size(bounds) === 0) return { residues: [], chains: [] };
+
     const start = Interval.start(bounds), end = Interval.end(bounds);
     const residues = [start], chains = [start];
 
@@ -122,18 +124,23 @@ function createModel(format: mmCIF_Format, bounds: Interval, previous?: Model):
         hierarchy,
         sequence: getSequence(format.data, entities, hierarchy),
         atomSiteConformation: getConformation(format, bounds),
-        coarseGrained: CoarseGrained.Empty,
+        coarseGrained: coarseGrainedFromIHM(format.data, entities),
         symmetry: getSymmetry(format),
         atomCount: Interval.size(bounds)
     };
 }
 
 function buildModels(format: mmCIF_Format): ReadonlyArray<Model> {
-    const models: Model[] = [];
     const atomCount = format.data.atom_site._rowCount;
+    const isIHM = format.data.ihm_model_list._rowCount > 0;
 
-    if (atomCount === 0) return models;
+    if (atomCount === 0) {
+        return isIHM
+            ? [createModel(format, Interval.Empty, void 0)]
+            : [];
+    }
 
+    const models: Model[] = [];
     let modelStart = 0;
     while (modelStart < atomCount) {
         const bounds = findModelBounds(format, modelStart);

+ 48 - 0
src/mol-model/structure/model/formats/mmcif/ihm.ts

@@ -4,3 +4,51 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
+import { mmCIF_Database as mmCIF, mmCIF_Schema } from 'mol-io/reader/cif/schema/mmcif'
+import CoarseGrained from '../../properties/coarse-grained'
+import { Entities } from '../../properties/common';
+import { Column } from 'mol-data/db';
+
+function coarseGrainedFromIHM(data: mmCIF, entities: Entities): CoarseGrained {
+    if (data.ihm_model_list._rowCount === 0) return CoarseGrained.Empty;
+
+    const { ihm_model_list, ihm_sphere_obj_site, ihm_gaussian_obj_site } = data;
+    const modelIndex = Column.createIndexer(ihm_model_list.model_id);
+
+    return {
+        isDefined: true,
+        modelList: ihm_model_list,
+        spheres: getSpheres(ihm_sphere_obj_site, entities, modelIndex),
+        gaussians: getGaussians(ihm_gaussian_obj_site, entities, modelIndex)
+    };
+}
+
+function getSpheres(data: mmCIF['ihm_sphere_obj_site'], entities: Entities, modelIndex: (id: number) => number): CoarseGrained.Spheres {
+    const { Cartn_x, Cartn_y, Cartn_z, object_radius: radius, rmsf } = data;
+    const x = Cartn_x.toArray({ array: Float32Array });
+    const y = Cartn_y.toArray({ array: Float32Array });
+    const z = Cartn_z.toArray({ array: Float32Array });
+    return { count: x.length, ...getCommonColumns(data, entities, modelIndex), x, y, z, radius, rmsf };
+}
+
+function getGaussians(data: mmCIF['ihm_gaussian_obj_site'], entities: Entities, modelIndex: (id: number) => number): CoarseGrained.Gaussians {
+    const { mean_Cartn_x, mean_Cartn_y, mean_Cartn_z, weight, covariance_matrix  } = data;
+    const x = mean_Cartn_x.toArray({ array: Float32Array });
+    const y = mean_Cartn_y.toArray({ array: Float32Array });
+    const z = mean_Cartn_z.toArray({ array: Float32Array });
+    return { count: x.length, ...getCommonColumns(data, entities, modelIndex), x, y, z, weight, covariance_matrix, matrix_space: mmCIF_Schema.ihm_gaussian_obj_site.covariance_matrix.space };
+}
+
+function getCommonColumns(data: mmCIF['ihm_sphere_obj_site'] | mmCIF['ihm_gaussian_obj_site'], entities: Entities, modelIndex: (id: number) => number) {
+    const { model_id, entity_id, seq_id_begin, seq_id_end, asym_id } = data;
+
+    return {
+        entityKey: Column.mapToArray(entity_id, id => entities.getEntityIndex(id), Int32Array),
+        modelKey: Column.mapToArray(model_id, modelIndex, Int32Array),
+        asym_id,
+        seq_id_begin,
+        seq_id_end
+    };
+}
+
+export { coarseGrainedFromIHM }

+ 18 - 14
src/mol-model/structure/model/properties/coarse-grained.ts

@@ -18,32 +18,36 @@ interface CoarseGrained {
 namespace CoarseGrained {
     export const Empty: CoarseGrained = { isDefined: false } as any;
 
-    interface Site {
-        // index to the Model.hierarchy.entities table
-        entityKey: number,
-        // index to the CoarseGrained.modelList table
-        modelKey: number,
+    export const enum ElementType { Sphere, Gaussian }
 
+    export interface SiteBase {
         asym_id: string,
         seq_id_begin: number,
-        seq_id_end: number,
-        x: number,
-        y: number,
-        z: number
+        seq_id_end: number
     }
 
-    export interface Sphere extends Site {
+    export interface Sphere extends SiteBase {
         radius: number,
         rmsf: number
     }
 
-    export interface Gaussian extends Site {
+    export interface Gaussian extends SiteBase {
         weight: number,
-        covarianceMatrix: Tensor.Data
+        covariance_matrix: Tensor.Data
     }
 
-    export type Spheres = { count: number} & { [P in keyof Sphere]: Column<Sphere[P]> }
-    export type Gaussians = { count: number} & { [P in keyof Gaussian]: Column<Gaussian[P]> }
+    type Common = {
+        count: number,
+        x: ArrayLike<number>,
+        y: ArrayLike<number>,
+        z: ArrayLike<number>,
+        modelKey: ArrayLike<number>,
+        entityKey: ArrayLike<number>
+    }
+
+    export type SiteBases =  Common & { [P in keyof SiteBase]: Column<SiteBase[P]> }
+    export type Spheres =  Common & { [P in keyof Sphere]: Column<Sphere[P]> }
+    export type Gaussians = Common & { matrix_space: Tensor.Space } & { [P in keyof Gaussian]: Column<Gaussian[P]> }
 }
 
 export default CoarseGrained;

+ 7 - 1
src/mol-model/structure/query/generators.ts

@@ -7,7 +7,7 @@
 import Query from './query'
 import Selection from './selection'
 import P from './properties'
-import { Structure, ElementSet, Element } from '../structure'
+import { Structure, ElementSet, Element, Unit } from '../structure'
 import { OrderedSet, Segmentation } from 'mol-data/int'
 
 export const all: Query.Provider = async (s, ctx) => Selection.Singletons(s, s.elements);
@@ -79,6 +79,9 @@ function atomGroupsSegmented({ entityTest, chainTest, residueTest, atomTest }: A
         for (let i = 0, _i = unitIds.length; i < _i; i++) {
             const unitId = unitIds[i];
             const unit = units[unitId];
+
+            if (unit.kind !== Unit.Kind.Atomic) continue;
+
             l.unit = unit;
             const set = ElementSet.groupAt(elements, i).elements;
 
@@ -171,6 +174,9 @@ function atomGroupsGrouped({ entityTest, chainTest, residueTest, atomTest, group
         for (let i = 0, _i = unitIds.length; i < _i; i++) {
             const unitId = unitIds[i];
             const unit = units[unitId];
+
+            if (unit.kind !== Unit.Kind.Atomic) continue;
+
             l.unit = unit;
             const set = ElementSet.groupAt(elements, i).elements;
 

+ 38 - 8
src/mol-model/structure/query/properties.ts

@@ -5,6 +5,7 @@
  */
 
 import { Element, Unit } from '../structure'
+import CoarseGrained from '../model/properties/coarse-grained';
 
 const constant = {
     true: Element.property(l => true),
@@ -16,6 +17,11 @@ function notAtomic(): never {
     throw 'Property only available for atomic models.';
 }
 
+function notCoarse(kind?: string): never {
+    if (!!kind) throw `Property only available for coarse models (${kind}).`;
+    throw `Property only available for coarse models.`;
+}
+
 const atom = {
     key: Element.property(l => l.element),
 
@@ -24,15 +30,15 @@ const atom = {
     y: Element.property(l => l.unit.y(l.element)),
     z: Element.property(l => l.unit.z(l.element)),
     id: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.conformation.atomId.value(l.element)),
-    occupancy: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.conformation.occupancy.value(l.element)),
-    B_iso_or_equiv: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.conformation.B_iso_or_equiv.value(l.element)),
+    occupancy: Element.property(l => !Unit.isAtomic(l.unit) ?  notAtomic() : l.unit.conformation.occupancy.value(l.element)),
+    B_iso_or_equiv: Element.property(l => !Unit.isAtomic(l.unit) ?  notAtomic() : l.unit.conformation.B_iso_or_equiv.value(l.element)),
 
     // Hierarchy
-    type_symbol: Element.property(l => l.unit.hierarchy.atoms.type_symbol.value(l.element)),
-    label_atom_id: Element.property(l => l.unit.hierarchy.atoms.label_atom_id.value(l.element)),
-    auth_atom_id: Element.property(l => l.unit.hierarchy.atoms.auth_atom_id.value(l.element)),
-    label_alt_id: Element.property(l => l.unit.hierarchy.atoms.label_alt_id.value(l.element)),
-    pdbx_formal_charge: Element.property(l => l.unit.hierarchy.atoms.pdbx_formal_charge.value(l.element))
+    type_symbol: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.hierarchy.atoms.type_symbol.value(l.element)),
+    label_atom_id: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.hierarchy.atoms.label_atom_id.value(l.element)),
+    auth_atom_id: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.hierarchy.atoms.auth_atom_id.value(l.element)),
+    label_alt_id: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.hierarchy.atoms.label_alt_id.value(l.element)),
+    pdbx_formal_charge: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.hierarchy.atoms.pdbx_formal_charge.value(l.element))
 }
 
 const residue = {
@@ -54,6 +60,29 @@ const chain = {
     label_entity_id: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.hierarchy.chains.label_entity_id.value(l.unit.chainIndex[l.element]))
 }
 
+const coarse_grained = {
+    modelKey: Element.property(l => !Unit.isCoarse(l.unit) ? notCoarse() : l.unit.siteBases.modelKey[l.element]),
+    entityKey: Element.property(l => !Unit.isCoarse(l.unit) ? notCoarse() : l.unit.siteBases.entityKey[l.element]),
+
+    x: atom.x,
+    y: atom.y,
+    z: atom.z,
+
+    asym_id: Element.property(l => !Unit.isCoarse(l.unit) ? notCoarse() : l.unit.siteBases.asym_id.value(l.element)),
+    seq_id_begin: Element.property(l => !Unit.isCoarse(l.unit) ? notCoarse() : l.unit.siteBases.seq_id_begin.value(l.element)),
+    seq_id_end: Element.property(l => !Unit.isCoarse(l.unit) ? notCoarse() : l.unit.siteBases.seq_id_end.value(l.element)),
+
+    sphere_radius: Element.property(l => !Unit.isCoarse(l.unit) || l.unit.elementType !== CoarseGrained.ElementType.Sphere
+        ? notCoarse('spheres') : l.unit.spheres.radius.value(l.element)),
+    sphere_rmsf: Element.property(l => !Unit.isCoarse(l.unit) || l.unit.elementType !== CoarseGrained.ElementType.Sphere
+        ? notCoarse('spheres') : l.unit.spheres.rmsf.value(l.element)),
+
+    gaussian_weight: Element.property(l => !Unit.isCoarse(l.unit) || l.unit.elementType !== CoarseGrained.ElementType.Gaussian
+        ? notCoarse('gaussians') : l.unit.gaussians.weight.value(l.element)),
+    gaussian_covariance_matrix: Element.property(l => !Unit.isCoarse(l.unit) || l.unit.elementType !== CoarseGrained.ElementType.Gaussian
+        ? notCoarse('gaussians') : l.unit.gaussians.covariance_matrix.value(l.element)),
+}
+
 function eK(l: Element.Location) { return !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.hierarchy.entityKey.value(l.unit.chainIndex[l.element]); }
 
 const entity = {
@@ -82,7 +111,8 @@ const Properties = {
     residue,
     chain,
     entity,
-    unit
+    unit,
+    coarse_grained
 }
 
 type Properties = typeof Properties

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

@@ -12,6 +12,7 @@ import Unit from './unit'
 import ElementSet from './element/set'
 import ElementGroup from './element/group'
 import Element from './element'
+import CoarseGrained from '../model/properties/coarse-grained';
 
 // A structure is a pair of "units" and an element set.
 // Each unit contains the data and transformation of its corresponding elements.
@@ -39,6 +40,20 @@ namespace Structure {
             builder.add(unit, unit.fullGroup);
         }
 
+        const cs = model.coarseGrained;
+        if (cs.isDefined) {
+            if (cs.spheres.count > 0) {
+                const group = ElementGroup.createNew(OrderedSet.ofBounds(0, cs.spheres.count));
+                const unit = Unit.createCoarse(model, SymmetryOperator.Default, group, CoarseGrained.ElementType.Sphere);
+                builder.add(unit, unit.fullGroup);
+            }
+            if (cs.gaussians.count > 0) {
+                const group = ElementGroup.createNew(OrderedSet.ofBounds(0, cs.gaussians.count));
+                const unit = Unit.createCoarse(model, SymmetryOperator.Default, group, CoarseGrained.ElementType.Gaussian);
+                builder.add(unit, unit.fullGroup);
+            }
+        }
+
         return builder.getStructure();
     }
 

+ 29 - 10
src/mol-model/structure/structure/unit.ts

@@ -9,6 +9,7 @@ import ElementGroup from './element/group'
 import { Model } from '../model'
 import { GridLookup3D } from 'mol-math/geometry'
 import { computeUnitBonds } from './element/properties/bonds/group-compute';
+import CoarseGrained from '../model/properties/coarse-grained';
 
 // A building block of a structure that corresponds to an atomic or a coarse grained representation
 // 'conveniently grouped together'.
@@ -29,9 +30,7 @@ namespace Unit {
         // Things like inter-unit bonds or spatial lookups
         // can be be implemented efficiently as "views" of the
         // full group.
-        readonly fullGroup: ElementGroup,
-
-        readonly hierarchy: Model['hierarchy'],
+        readonly fullGroup: ElementGroup
     }
 
     // A bulding block of a structure that corresponds
@@ -47,16 +46,21 @@ namespace Unit {
         // Reference some commonly accessed things for faster access.
         readonly residueIndex: ArrayLike<number>,
         readonly chainIndex: ArrayLike<number>,
-        readonly conformation: Model['atomSiteConformation']
+        readonly conformation: Model['atomSiteConformation'],
+        readonly hierarchy: Model['hierarchy']
     }
 
     // Coarse grained representations.
-    // TODO: can we use the ArrayMapping here?
     export interface Coarse extends Base  {
-        readonly kind: Unit.Kind.Coarse
+        readonly kind: Unit.Kind.Coarse,
+        readonly elementType: CoarseGrained.ElementType,
+
+        readonly siteBases: CoarseGrained.SiteBases,
+        readonly spheres: CoarseGrained.Spheres,
+        readonly gaussians: CoarseGrained.Gaussians
     }
 
-    export function createAtomic(model: Model, operator: SymmetryOperator, fullGroup: ElementGroup): Unit {
+    export function createAtomic(model: Model, operator: SymmetryOperator, fullGroup: ElementGroup): Unit.Atomic {
         const h = model.hierarchy;
         const { invariantPosition, position, x, y, z } = SymmetryOperator.createMapping(operator, model.atomSiteConformation);
 
@@ -75,14 +79,29 @@ namespace Unit {
         };
     }
 
-    export function createCoarse(model: Model, operator: SymmetryOperator, fullGroup: ElementGroup): Unit {
-        throw 'not implemented'
+    export function createCoarse(model: Model, operator: SymmetryOperator, fullGroup: ElementGroup, elementType: CoarseGrained.ElementType): Unit.Coarse {
+        const siteBases = elementType === CoarseGrained.ElementType.Sphere ? model.coarseGrained.spheres : model.coarseGrained.gaussians;
+        const { invariantPosition, position, x, y, z } = SymmetryOperator.createMapping(operator, siteBases);
+
+        return {
+            model,
+            kind: Kind.Coarse,
+            elementType,
+            operator,
+            fullGroup,
+            siteBases,
+            spheres: model.coarseGrained.spheres,
+            gaussians: model.coarseGrained.gaussians,
+            invariantPosition,
+            position,
+            x, y, z
+        };
     }
 
     export function withOperator(unit: Unit, operator: SymmetryOperator): Unit {
         switch (unit.kind) {
             case Kind.Atomic: return createAtomic(unit.model, SymmetryOperator.compose(unit.operator, operator), unit.fullGroup);
-            case Kind.Coarse: return createCoarse(unit.model, SymmetryOperator.compose(unit.operator, operator), unit.fullGroup);
+            case Kind.Coarse: return createCoarse(unit.model, SymmetryOperator.compose(unit.operator, operator), unit.fullGroup, unit.elementType);
         }
     }
 

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

@@ -148,7 +148,7 @@ export namespace PropertyAccess {
 
         let vA = 0, cC = 0, rC = 0;
         for (let i = 0, _i = unitIds.length; i < _i; i++) {
-            const unit = units[unitIds[i]];
+            const unit = units[unitIds[i]] as Unit.Atomic;
             l.unit = unit;
             const set = ElementSet.groupAt(elements, i);