Ver Fonte

Merge branch 'master' into bond-repr

# Conflicts:
#	src/apps/structure-info/model.ts
#	src/mol-geo/theme/structure/color/chain-id.ts
#	src/mol-model/structure/model/model.ts
#	src/mol-model/structure/structure/structure.ts
Alexander Rose há 6 anos atrás
pai
commit
1c0043b789
32 ficheiros alterados com 723 adições e 513 exclusões
  1. 3 2
      src/apps/structure-info/model.ts
  2. 3 3
      src/mol-app/ui/visualization/sequence-view.tsx
  3. 3 3
      src/mol-data/int/impl/segmentation.ts
  4. 2 2
      src/mol-data/int/segmentation.ts
  5. 3 3
      src/mol-geo/representation/structure/index.ts
  6. 4 3
      src/mol-geo/theme/structure/color/chain-id.ts
  7. 3 3
      src/mol-geo/theme/structure/size/physical.ts
  8. 54 0
      src/mol-model/structure/export/categories/atom_site.ts
  9. 17 57
      src/mol-model/structure/export/mmcif.ts
  10. 11 1
      src/mol-model/structure/model/formats/mmcif.ts
  11. 3 2
      src/mol-model/structure/model/formats/mmcif/assembly.ts
  12. 2 268
      src/mol-model/structure/model/formats/mmcif/bonds.ts
  13. 150 0
      src/mol-model/structure/model/formats/mmcif/bonds/comp.ts
  14. 192 0
      src/mol-model/structure/model/formats/mmcif/bonds/struct_conn.ts
  15. 4 3
      src/mol-model/structure/model/formats/mmcif/util.ts
  16. 11 4
      src/mol-model/structure/model/model.ts
  17. 8 0
      src/mol-model/structure/model/properties/custom.ts
  18. 25 0
      src/mol-model/structure/model/properties/custom/collection.ts
  19. 21 0
      src/mol-model/structure/model/properties/custom/descriptor.ts
  20. 0 2
      src/mol-model/structure/query.ts
  21. 1 2
      src/mol-model/structure/query/generators.ts
  22. 1 2
      src/mol-model/structure/query/predicates.ts
  23. 1 117
      src/mol-model/structure/query/properties.ts
  24. 2 1
      src/mol-model/structure/structure.ts
  25. 126 0
      src/mol-model/structure/structure/properties.ts
  26. 10 2
      src/mol-model/structure/structure/structure.ts
  27. 1 1
      src/mol-model/structure/structure/symmetry.ts
  28. 14 0
      src/mol-model/structure/structure/unit/links/inter-compute.ts
  29. 17 1
      src/mol-model/structure/structure/unit/links/intra-compute.ts
  30. 9 9
      src/mol-view/label.ts
  31. 12 12
      src/perf-tests/structure.ts
  32. 10 10
      src/servers/model/server/api.ts

+ 3 - 2
src/apps/structure-info/model.ts

@@ -9,7 +9,8 @@ import * as argparse from 'argparse'
 require('util.promisify').shim();
 
 import { CifFrame } from 'mol-io/reader/cif'
-import { Model, Structure, Element, Unit, Queries, Format } from 'mol-model/structure'
+import { Model, Structure, Element, Unit, Format, StructureProperties } from 'mol-model/structure'
+// import { Run, Progress } from 'mol-task'
 import { OrderedSet } from 'mol-data/int';
 import { Table } from 'mol-data/db';
 import { openCif, downloadCif } from './helpers';
@@ -164,7 +165,7 @@ export function printUnits(structure: Structure) {
         } else if (Unit.isCoarse(l.unit)) {
             console.log(`Coarse unit ${unit.id} ${unit.conformation.operator.name} (${Unit.isSpheres(l.unit) ? 'spheres' : 'gaussians'}): ${size} elements.`);
 
-            const props = Queries.props.coarse;
+            const props = StructureProperties.coarse;
             const seq = l.unit.model.sequence;
 
             for (let j = 0, _j = Math.min(size, 3); j < _j; j++) {

+ 3 - 3
src/mol-app/ui/visualization/sequence-view.tsx

@@ -7,7 +7,7 @@
 import * as React from 'react'
 import { View } from '../view';
 import { SequenceViewController } from '../../controller/visualization/sequence-view';
-import { Structure, StructureSequence, Queries, Selection } from 'mol-model/structure';
+import { Structure, StructureSequence, Queries, Selection, StructureProperties } from 'mol-model/structure';
 import { Context } from '../../context/context';
 import { InteractivityEvents } from '../../event/basic';
 import { SyncRuntimeContext } from 'mol-task/execution/synchronous';
@@ -27,8 +27,8 @@ export class SequenceView extends View<SequenceViewController, {}, {}> {
 
 function createQuery(entityId: string, label_seq_id: number) {
     return Queries.generators.atoms({
-        entityTest: l => Queries.props.entity.id(l) === entityId,
-        residueTest: l => Queries.props.residue.label_seq_id(l) === label_seq_id
+        entityTest: l => StructureProperties.entity.id(l) === entityId,
+        residueTest: l => StructureProperties.residue.label_seq_id(l) === label_seq_id
     });
 }
 

+ 3 - 3
src/mol-data/int/impl/segmentation.ts

@@ -54,7 +54,7 @@ export class SegmentIterator<T extends number = number> implements Iterator<Segs
     private segmentMin = 0;
     private segmentMax = 0;
     private setRange = Interval.Empty;
-    private value: Segs.Segment<T> = { index: 0, start: 0, end: 0 };
+    private value: Segs.Segment<T> = { index: 0, start: 0 as T, end: 0 as T };
 
     hasNext: boolean = false;
 
@@ -75,8 +75,8 @@ export class SegmentIterator<T extends number = number> implements Iterator<Segs
         const segmentEnd = this.segments[this.segmentMin + 1];
         // TODO: add optimized version for interval and array?
         const setEnd = OrderedSet.findPredecessorIndexInInterval(this.set, segmentEnd, this.setRange);
-        this.value.start = Interval.start(this.setRange);
-        this.value.end = setEnd;
+        this.value.start = Interval.start(this.setRange) as T;
+        this.value.end = setEnd as T;
         this.setRange = Interval.ofBounds(setEnd, Interval.end(this.setRange));
         return setEnd > this.value.start;
     }

+ 2 - 2
src/mol-data/int/segmentation.ts

@@ -9,7 +9,7 @@ import OrderedSet from './ordered-set'
 import * as Impl from './impl/segmentation'
 
 namespace Segmentation {
-    export interface Segment<T extends number = number> { index: number, start: number, end: number }
+    export interface Segment<T extends number = number> { index: number, start: T, end: T }
 
     export const create: <T extends number = number>(segs: ArrayLike<T>) => Segmentation<T> = Impl.create as any;
     export const ofOffsets: <T extends number = number>(offsets: ArrayLike<T>, bounds: Interval) => Segmentation<T> = Impl.ofOffsets as any;
@@ -19,7 +19,7 @@ namespace Segmentation {
     export const projectValue: <T extends number = number>(segs: Segmentation<T>, set: OrderedSet<T>, value: T) => Interval = Impl.projectValue as any;
 
     // Segment iterator that mutates a single segment object to mark all the segments.
-    export const transientSegments: <T extends number = number>(segs: Segmentation<T>, set: OrderedSet<T>, segment?: Segment<T>) => Impl.SegmentIterator = Impl.segments as any;
+    export const transientSegments: <T extends number = number>(segs: Segmentation<T>, set: OrderedSet<T>, segment?: Segment<T>) => Impl.SegmentIterator<T> = Impl.segments as any;
 }
 
 interface Segmentation<T extends number = number> {

+ 3 - 3
src/mol-geo/representation/structure/index.ts

@@ -5,7 +5,7 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { Structure, StructureSymmetry, Unit } from 'mol-model/structure';
+import { Structure, Unit } from 'mol-model/structure';
 import { Task } from 'mol-task'
 import { RenderObject } from 'mol-gl/render-object';
 import { Representation, RepresentationProps, Visual } from '..';
@@ -105,7 +105,7 @@ export function StructureUnitsRepresentation<P extends StructureProps>(visualCto
 
         return Task.create('Creating StructureRepresentation', async ctx => {
             if (!_structure) {
-                _groups = StructureSymmetry.getTransformGroups(structure);
+                _groups = structure.unitSymmetryGroups;
                 for (let i = 0; i < _groups.length; i++) {
                     const group = _groups[i];
                     const visual = visualCtor()
@@ -116,7 +116,7 @@ export function StructureUnitsRepresentation<P extends StructureProps>(visualCto
                 if (_structure.hashCode === structure.hashCode) {
                     await update(_props)
                 } else {
-                    _groups = StructureSymmetry.getTransformGroups(structure);
+                    _groups = structure.unitSymmetryGroups;
                     const newGroups: Unit.SymmetryGroup[] = []
                     const oldUnitsVisuals = visuals
                     visuals = new Map()

+ 4 - 3
src/mol-geo/theme/structure/color/chain-id.ts

@@ -4,7 +4,7 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Unit, Queries, Element } from 'mol-model/structure';
+import { Unit, StructureProperties, Element } from 'mol-model/structure';
 
 import { StructureColorDataProps } from '.';
 import { ColorData, createElementColor, createUniformColor } from '../../../util/color-data';
@@ -13,11 +13,12 @@ import { ColorScale } from 'mol-util/color';
 function getAsymId(unit: Unit): Element.Property<string> {
     switch (unit.kind) {
         case Unit.Kind.Atomic:
-            return Queries.props.chain.label_asym_id
+            return StructureProperties.chain.label_asym_id
         case Unit.Kind.Spheres:
         case Unit.Kind.Gaussians:
-            return Queries.props.coarse.asym_id
+            return StructureProperties.coarse.asym_id
     }
+    throw new Error('unhandled unit kind')
 }
 
 export function chainIdColorData(props: StructureColorDataProps, locationFn: (l: Element.Location, renderElementIdx: number) => void, colorData?: ColorData) {

+ 3 - 3
src/mol-geo/theme/structure/size/physical.ts

@@ -4,15 +4,15 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Element, Unit, Queries } from 'mol-model/structure';
+import { Element, Unit, StructureProperties } from 'mol-model/structure';
 import { StructureSizeDataProps } from '.';
 import { createAttributeSize } from '../../../util/size-data';
 
 export function getPhysicalRadius(unit: Unit): Element.Property<number> {
     if (Unit.isAtomic(unit)) {
-        return Queries.props.atom.vdw_radius
+        return StructureProperties.atom.vdw_radius
     } else if (Unit.isSpheres(unit)) {
-        return Queries.props.coarse.sphere_radius
+        return StructureProperties.coarse.sphere_radius
     } else {
         return () => 0
     }

+ 54 - 0
src/mol-model/structure/export/categories/atom_site.ts

@@ -0,0 +1,54 @@
+/**
+ * Copyright (c) 2017-2018 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>
+ */
+
+import { CifWriter } from 'mol-io/writer/cif';
+import { Element, Structure, StructureProperties as P } from '../../structure';
+import { CifExportContext } from '../mmcif';
+import CifField = CifWriter.Field
+import CifCategory = CifWriter.Category
+import E = CifWriter.Encodings
+
+const atom_site_fields: CifField<Element.Location>[] = [
+    CifField.str('group_PDB', P.residue.group_PDB),
+    CifField.int('id', P.atom.id, { encoder: E.deltaRLE }),
+    CifField.str('type_symbol', P.atom.type_symbol as any),
+    CifField.str('label_atom_id', P.atom.label_atom_id),
+    CifField.str('label_alt_id', P.atom.label_alt_id),
+
+    CifField.str('label_comp_id', P.residue.label_comp_id),
+    CifField.int('label_seq_id', P.residue.label_seq_id, { encoder: E.deltaRLE }),
+    CifField.str('pdbx_PDB_ins_code', P.residue.pdbx_PDB_ins_code),
+
+    CifField.str('label_asym_id', P.chain.label_asym_id),
+    CifField.str('label_entity_id', P.chain.label_entity_id),
+
+    CifField.float('Cartn_x', P.atom.x, { digitCount: 3, encoder: E.fixedPoint3 }),
+    CifField.float('Cartn_y', P.atom.y, { digitCount: 3, encoder: E.fixedPoint3 }),
+    CifField.float('Cartn_z', P.atom.z, { digitCount: 3, encoder: E.fixedPoint3 }),
+    CifField.float('occupancy', P.atom.occupancy, { digitCount: 2, encoder: E.fixedPoint2 }),
+    CifField.int('pdbx_formal_charge', P.atom.pdbx_formal_charge, { encoder: E.deltaRLE }),
+
+    CifField.str('auth_atom_id', P.atom.auth_atom_id),
+    CifField.str('auth_comp_id', P.residue.auth_comp_id),
+    CifField.int('auth_seq_id', P.residue.auth_seq_id, { encoder: E.deltaRLE }),
+    CifField.str('auth_asym_id', P.chain.auth_asym_id),
+
+    CifField.int('pdbx_PDB_model_num', P.unit.model_num, { encoder: E.deltaRLE }),
+    CifField.str<Element.Location, Structure>('operator_name', P.unit.operator_name, {
+        shouldInclude: structure => structure.units.some(u => !u.conformation.operator.isIdentity)
+    })
+];
+
+export function _atom_site({ structure }: CifExportContext): CifCategory {
+    return {
+        data: structure,
+        name: 'atom_site',
+        fields: atom_site_fields,
+        rowCount: structure.elementCount,
+        keys: () => structure.elementLocations()
+    }
+}

+ 17 - 57
src/mol-model/structure/export/mmcif.ts

@@ -7,53 +7,19 @@
 
 import { CifWriter } from 'mol-io/writer/cif'
 import { mmCIF_Schema } from 'mol-io/reader/cif/schema/mmcif'
-import { Structure, Element } from '../structure'
+import { Structure } from '../structure'
 import { Model } from '../model'
-import P from '../query/properties'
+import { _atom_site } from './categories/atom_site';
 
-interface Context {
+export interface CifExportContext {
     structure: Structure,
     model: Model
 }
 
-import CifField = CifWriter.Field
 import CifCategory = CifWriter.Category
 
-import E = CifWriter.Encodings
-
-const atom_site_fields: CifField<Element.Location>[] = [
-    CifField.str('group_PDB', P.residue.group_PDB),
-    CifField.int('id', P.atom.id, { encoder: E.deltaRLE }),
-    CifField.str('type_symbol', P.atom.type_symbol as any),
-    CifField.str('label_atom_id', P.atom.label_atom_id),
-    CifField.str('label_alt_id', P.atom.label_alt_id),
-
-    CifField.str('label_comp_id', P.residue.label_comp_id),
-    CifField.int('label_seq_id', P.residue.label_seq_id, { encoder: E.deltaRLE }),
-    CifField.str('pdbx_PDB_ins_code', P.residue.pdbx_PDB_ins_code),
-
-    CifField.str('label_asym_id', P.chain.label_asym_id),
-    CifField.str('label_entity_id', P.chain.label_entity_id),
-
-    CifField.float('Cartn_x', P.atom.x, { digitCount: 3, encoder: E.fixedPoint3 }),
-    CifField.float('Cartn_y', P.atom.y, { digitCount: 3, encoder: E.fixedPoint3 }),
-    CifField.float('Cartn_z', P.atom.z, { digitCount: 3, encoder: E.fixedPoint3 }),
-    CifField.float('occupancy', P.atom.occupancy, { digitCount: 2, encoder: E.fixedPoint2 }),
-    CifField.int('pdbx_formal_charge', P.atom.pdbx_formal_charge, { encoder: E.deltaRLE }),
-
-    CifField.str('auth_atom_id', P.atom.auth_atom_id),
-    CifField.str('auth_comp_id', P.residue.auth_comp_id),
-    CifField.int('auth_seq_id', P.residue.auth_seq_id, { encoder: E.deltaRLE }),
-    CifField.str('auth_asym_id', P.chain.auth_asym_id),
-
-    CifField.int('pdbx_PDB_model_num', P.unit.model_num, { encoder: E.deltaRLE }),
-    CifField.str<Element.Location, Structure>('operator_name', P.unit.operator_name, {
-        shouldInclude: structure => structure.units.some(u => !u.conformation.operator.isIdentity)
-    })
-];
-
-function copy_mmCif_cat(name: keyof mmCIF_Schema) {
-    return ({ model }: Context) => {
+function copy_mmCif_category(name: keyof mmCIF_Schema) {
+    return ({ model }: CifExportContext) => {
         if (model.sourceData.kind !== 'mmCIF') return CifCategory.Empty;
         const table = model.sourceData.data[name];
         if (!table || !table._rowCount) return CifCategory.Empty;
@@ -61,32 +27,20 @@ function copy_mmCif_cat(name: keyof mmCIF_Schema) {
     };
 }
 
-function _entity({ model, structure }: Context): CifCategory {
+function _entity({ model, structure }: CifExportContext): CifCategory {
     const keys = Structure.getEntityKeys(structure);
     return CifCategory.ofTable('entity', model.entities.data, keys);
 }
 
-function _atom_site({ structure }: Context): CifCategory {
-    return {
-        data: structure,
-        name: 'atom_site',
-        fields: atom_site_fields,
-        rowCount: structure.elementCount,
-        keys: () => structure.elementLocations()
-    }
-}
-
 const Categories = [
-    copy_mmCif_cat('entry'),
-    copy_mmCif_cat('exptl'),
-    copy_mmCif_cat('cell'),
-    copy_mmCif_cat('symmetry'),
+    copy_mmCif_category('entry'),
+    copy_mmCif_category('exptl'),
+    copy_mmCif_category('cell'),
+    copy_mmCif_category('symmetry'),
     _entity,
     _atom_site
 ];
 
-mmCIF_Schema
-
 namespace _Filters {
     export const AtomSitePositionsFieldNames = new Set<string>(<(keyof typeof mmCIF_Schema.atom_site)[]>['id', 'Cartn_x', 'Cartn_y', 'Cartn_z']);
 }
@@ -104,11 +58,17 @@ export function encode_mmCIF_categories(encoder: CifWriter.Encoder, structure: S
     if (models.length !== 1) throw 'Can\'t export stucture composed from multiple models.';
     const model = models[0];
 
-    const ctx: Context[] = [{ structure, model }];
+    const ctx: CifExportContext[] = [{ structure, model }];
 
     for (const cat of Categories) {
         encoder.writeCategory(cat, ctx);
     }
+    for (const customProp of model.customProperties.all) {
+        const cats = customProp.cifExport.categoryProvider(ctx[0]);
+        for (const cat of cats) {
+            encoder.writeCategory(cat, ctx);
+        }
+    }
 }
 
 function to_mmCIF(name: string, structure: Structure, asBinary = false) {

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

@@ -24,8 +24,10 @@ import { getSequence } from './mmcif/sequence';
 import { sortAtomSite } from './mmcif/sort';
 import { mmCIF_Database, mmCIF_Schema } from 'mol-io/reader/cif/schema/mmcif';
 import { Element } from '../../../structure'
+import { CustomProperties } from '../properties/custom';
 
 import mmCIF_Format = Format.mmCIF
+import { ComponentBond } from './mmcif/bonds';
 type AtomSite = mmCIF_Database['atom_site']
 
 function findModelEnd({ data }: mmCIF_Format, startIndex: number) {
@@ -208,6 +210,7 @@ function createModel(format: mmCIF_Format, atom_site: AtomSite, previous?: Model
         sourceData: format,
         modelNum: atom_site.pdbx_PDB_model_num.value(0),
         entities,
+        symmetry: getSymmetry(format),
         atomicHierarchy,
         sequence: getSequence(format.data, entities, atomicHierarchy, modifiedResidueNameMap),
         atomicConformation: getConformation(atom_site),
@@ -218,10 +221,16 @@ function createModel(format: mmCIF_Format, atom_site: AtomSite, previous?: Model
             modifiedResidueNameMap,
             asymIdSerialMap
         },
-        symmetry: getSymmetry(format)
+        customProperties: new CustomProperties(),
+        _staticPropertyData: Object.create(null),
+        _dynamicPropertyData: Object.create(null)
     };
 }
 
+function attachProps(model: Model) {
+    ComponentBond.attachFromMmCif(model);
+}
+
 function buildModels(format: mmCIF_Format): Task<ReadonlyArray<Model>> {
     return Task.create('Create mmCIF Model', async ctx => {
         const atomCount = format.data.atom_site._rowCount;
@@ -239,6 +248,7 @@ function buildModels(format: mmCIF_Format): Task<ReadonlyArray<Model>> {
             const modelEnd = findModelEnd(format, modelStart);
             const atom_site = await sortAtomSite(ctx, format.data.atom_site, modelStart, modelEnd);
             const model = createModel(format, atom_site, models.length > 0 ? models[models.length - 1] : void 0);
+            attachProps(model);
             models.push(model);
             modelStart = modelEnd;
         }

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

@@ -11,6 +11,7 @@ import { Assembly, OperatorGroup, OperatorGroups } from '../../properties/symmet
 import { Queries as Q, Query } from '../../../query'
 
 import mmCIF_Format = Format.mmCIF
+import { StructureProperties } from '../../../structure';
 
 export function createAssemblies(format: mmCIF_Format): ReadonlyArray<Assembly> {
     const { pdbx_struct_assembly } = format.data;
@@ -58,8 +59,8 @@ function operatorGroupsProvider(generators: Generator[], matrices: Matrices): ()
             const operatorNames = expandOperators(operatorList);
             const operators = getAssemblyOperators(matrices, operatorNames, operatorOffset);
             const selector = Query(Q.generators.atoms({ chainTest: Q.pred.and(
-                Q.pred.eq(Q.props.unit.operator_name, SymmetryOperator.DefaultName),
-                Q.pred.inSet(Q.props.chain.label_asym_id, gen.asymIds)
+                Q.pred.eq(StructureProperties.unit.operator_name, SymmetryOperator.DefaultName),
+                Q.pred.inSet(StructureProperties.chain.label_asym_id, gen.asymIds)
             )}));
             groups[groups.length] = { selector, operators };
             operatorOffset += operators.length;

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

@@ -5,271 +5,5 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import Model from '../../model'
-import { LinkType } from '../../types'
-import { findEntityIdByAsymId, findAtomIndexByLabelName } from './util'
-import { Column } from 'mol-data/db'
-
-export interface StructConn {
-    getResidueEntries(residueAIndex: number, residueBIndex: number): ReadonlyArray<StructConn.Entry>
-    getAtomEntries(atomIndex: number): ReadonlyArray<StructConn.Entry>
-}
-
-export interface ComponentBond {
-    entries: Map<string, ComponentBond.Entry>
-}
-
-export namespace StructConn {
-    function _resKey(rA: number, rB: number) {
-        if (rA < rB) return `${rA}-${rB}`;
-        return `${rB}-${rA}`;
-    }
-    const _emptyEntry: Entry[] = [];
-
-    class StructConnImpl implements StructConn {
-        private _residuePairIndex: Map<string, StructConn.Entry[]> | undefined = void 0;
-        private _atomIndex: Map<number, StructConn.Entry[]> | undefined = void 0;
-
-        private getResiduePairIndex() {
-            if (this._residuePairIndex) return this._residuePairIndex;
-            this._residuePairIndex = new Map();
-            for (const e of this.entries) {
-                const ps = e.partners;
-                const l = ps.length;
-                for (let i = 0; i < l - 1; i++) {
-                    for (let j = i + i; j < l; j++) {
-                        const key = _resKey(ps[i].residueIndex, ps[j].residueIndex);
-                        if (this._residuePairIndex.has(key)) {
-                            this._residuePairIndex.get(key)!.push(e);
-                        } else {
-                            this._residuePairIndex.set(key, [e]);
-                        }
-                    }
-                }
-            }
-            return this._residuePairIndex;
-        }
-
-        private getAtomIndex() {
-            if (this._atomIndex) return this._atomIndex;
-            this._atomIndex = new Map();
-            for (const e of this.entries) {
-                for (const p of e.partners) {
-                    const key = p.atomIndex;
-                    if (this._atomIndex.has(key)) {
-                        this._atomIndex.get(key)!.push(e);
-                    } else {
-                        this._atomIndex.set(key, [e]);
-                    }
-                }
-            }
-            return this._atomIndex;
-        }
-
-
-        getResidueEntries(residueAIndex: number, residueBIndex: number): ReadonlyArray<StructConn.Entry> {
-            return this.getResiduePairIndex().get(_resKey(residueAIndex, residueBIndex)) || _emptyEntry;
-        }
-
-        getAtomEntries(atomIndex: number): ReadonlyArray<StructConn.Entry> {
-            return this.getAtomIndex().get(atomIndex) || _emptyEntry;
-        }
-
-        constructor(public entries: StructConn.Entry[]) {
-        }
-    }
-
-    export interface Entry {
-        distance: number,
-        order: number,
-        flags: number,
-        partners: { residueIndex: number, atomIndex: number, symmetry: string }[]
-    }
-
-    type StructConnType =
-        | 'covale'
-        | 'covale_base'
-        | 'covale_phosphate'
-        | 'covale_sugar'
-        | 'disulf'
-        | 'hydrog'
-        | 'metalc'
-        | 'mismat'
-        | 'modres'
-        | 'saltbr'
-
-    export const PropName = '__StructConn__';
-    export function fromModel(model: Model): StructConn | undefined {
-        if (model.properties[PropName]) return model.properties[PropName];
-
-        if (model.sourceData.kind !== 'mmCIF') return;
-        const { struct_conn } = model.sourceData.data;
-        if (!struct_conn._rowCount) return void 0;
-
-        const { conn_type_id, pdbx_dist_value, pdbx_value_order } = struct_conn;
-        const p1 = {
-            label_asym_id: struct_conn.ptnr1_label_asym_id,
-            label_comp_id: struct_conn.ptnr1_label_comp_id,
-            label_seq_id: struct_conn.ptnr1_label_seq_id,
-            label_atom_id: struct_conn.ptnr1_label_atom_id,
-            label_alt_id: struct_conn.pdbx_ptnr1_label_alt_id,
-            ins_code: struct_conn.pdbx_ptnr1_PDB_ins_code,
-            symmetry: struct_conn.ptnr1_symmetry
-        };
-        const p2: typeof p1 = {
-            label_asym_id: struct_conn.ptnr2_label_asym_id,
-            label_comp_id: struct_conn.ptnr2_label_comp_id,
-            label_seq_id: struct_conn.ptnr2_label_seq_id,
-            label_atom_id: struct_conn.ptnr2_label_atom_id,
-            label_alt_id: struct_conn.pdbx_ptnr2_label_alt_id,
-            ins_code: struct_conn.pdbx_ptnr2_PDB_ins_code,
-            symmetry: struct_conn.ptnr2_symmetry
-        };
-
-        const _p = (row: number, ps: typeof p1) => {
-            if (ps.label_asym_id.valueKind(row) !== Column.ValueKind.Present) return void 0;
-            const asymId = ps.label_asym_id.value(row)
-            const residueIndex = model.atomicHierarchy.findResidueKey(
-                findEntityIdByAsymId(model, asymId),
-                ps.label_comp_id.value(row),
-                asymId,
-                ps.label_seq_id.value(row),
-                ps.ins_code.value(row)
-            );
-            if (residueIndex < 0) return void 0;
-            const atomName = ps.label_atom_id.value(row);
-            // turns out "mismat" records might not have atom name value
-            if (!atomName) return void 0;
-            const atomIndex = findAtomIndexByLabelName(model, residueIndex, atomName, ps.label_alt_id.value(row));
-            if (atomIndex < 0) return void 0;
-            return { residueIndex, atomIndex, symmetry: ps.symmetry.value(row) || '1_555' };
-        }
-
-        const _ps = (row: number) => {
-            const ret = [];
-            let p = _p(row, p1);
-            if (p) ret.push(p);
-            p = _p(row, p2);
-            if (p) ret.push(p);
-            return ret;
-        }
-
-        const entries: StructConn.Entry[] = [];
-        for (let i = 0; i < struct_conn._rowCount; i++) {
-            const partners = _ps(i);
-            if (partners.length < 2) continue;
-
-            const type = conn_type_id.value(i)! as StructConnType;
-            const orderType = (pdbx_value_order.value(i) || '').toLowerCase();
-            let flags = LinkType.Flag.None;
-            let order = 1;
-
-            switch (orderType) {
-                case 'sing': order = 1; break;
-                case 'doub': order = 2; break;
-                case 'trip': order = 3; break;
-                case 'quad': order = 4; break;
-            }
-
-            switch (type) {
-                case 'covale':
-                case 'covale_base':
-                case 'covale_phosphate':
-                case 'covale_sugar':
-                case 'modres':
-                    flags = LinkType.Flag.Covalent;
-                    break;
-                case 'disulf': flags = LinkType.Flag.Covalent | LinkType.Flag.Sulfide; break;
-                case 'hydrog': flags = LinkType.Flag.Hydrogen; break;
-                case 'metalc': flags = LinkType.Flag.MetallicCoordination; break;
-                case 'saltbr': flags = LinkType.Flag.Ion; break;
-            }
-
-            entries.push({ flags, order, distance: pdbx_dist_value.value(i), partners });
-        }
-
-        const ret = new StructConnImpl(entries);
-        model.properties[PropName] = ret;
-        return ret;
-    }
-}
-
-export namespace ComponentBond {
-    export class ComponentBondImpl implements ComponentBond {
-        entries: Map<string, ComponentBond.Entry> = new Map();
-
-        addEntry(id: string) {
-            let e = new Entry(id);
-            this.entries.set(id, e);
-            return e;
-        }
-    }
-
-    export class Entry implements Entry {
-        map: Map<string, Map<string, { order: number, flags: number }>> = new Map();
-
-        add(a: string, b: string, order: number, flags: number, swap = true) {
-            let e = this.map.get(a);
-            if (e !== void 0) {
-                let f = e.get(b);
-                if (f === void 0) {
-                    e.set(b, { order, flags });
-                }
-            } else {
-                let map = new Map<string, { order: number, flags: number }>();
-                map.set(b, { order, flags });
-                this.map.set(a, map);
-            }
-
-            if (swap) this.add(b, a, order, flags, false);
-        }
-
-        constructor(public id: string) {
-        }
-    }
-
-    export const PropName = '__ComponentBond__';
-    export function fromModel(model: Model): ComponentBond | undefined {
-        if (model.properties[PropName]) return model.properties[PropName];
-
-        if (model.sourceData.kind !== 'mmCIF') return
-        const { chem_comp_bond } = model.sourceData.data;
-        if (!chem_comp_bond._rowCount) return void 0;
-
-        let compBond = new ComponentBondImpl();
-
-        const { comp_id, atom_id_1, atom_id_2, value_order, pdbx_aromatic_flag, _rowCount: rowCount } = chem_comp_bond;
-
-        let entry = compBond.addEntry(comp_id.value(0)!);
-
-        for (let i = 0; i < rowCount; i++) {
-
-            const id = comp_id.value(i)!;
-            const nameA = atom_id_1.value(i)!;
-            const nameB = atom_id_2.value(i)!;
-            const order = value_order.value(i)!;
-            const aromatic = pdbx_aromatic_flag.value(i) === 'Y';
-
-            if (entry.id !== id) {
-                entry = compBond.addEntry(id);
-            }
-
-            let flags: number = LinkType.Flag.Covalent;
-            let ord = 1;
-            if (aromatic) flags |= LinkType.Flag.Aromatic;
-            switch (order.toLowerCase()) {
-                case 'doub':
-                case 'delo':
-                    ord = 2;
-                    break;
-                case 'trip': ord = 3; break;
-                case 'quad': ord = 4; break;
-            }
-
-            entry.add(nameA, nameB, ord, flags);
-        }
-
-        model.properties[PropName] = compBond;
-        return compBond;
-    }
-}
+export * from './bonds/comp'
+export * from './bonds/struct_conn'

+ 150 - 0
src/mol-model/structure/model/formats/mmcif/bonds/comp.ts

@@ -0,0 +1,150 @@
+/**
+ * Copyright (c) 2017-2018 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>
+ */
+
+import Model from '../../../model'
+import { LinkType } from '../../../types'
+import { ModelPropertyDescriptor } from '../../../properties/custom';
+import { mmCIF_Database } from 'mol-io/reader/cif/schema/mmcif';
+import { Structure, Unit, StructureProperties, Element } from '../../../../structure';
+import { Segmentation } from 'mol-data/int';
+import { CifWriter } from 'mol-io/writer/cif'
+
+export interface ComponentBond {
+    entries: Map<string, ComponentBond.Entry>
+}
+
+export namespace ComponentBond {
+    export const Descriptor: ModelPropertyDescriptor = {
+        isStatic: true,
+        name: 'chem_comp_bond',
+        cifExport: {
+            categoryNames: ['chem_comp_bond'],
+            categoryProvider(ctx) {
+                const comp_names = getUniqueResidueNames(ctx.structure);
+                const chem_comp_bond = getChemCompBond(ctx.model);
+
+                if (!chem_comp_bond) return [];
+
+                const { comp_id, _rowCount } = chem_comp_bond;
+                const indices: number[] = [];
+                for (let i = 0; i < _rowCount; i++) {
+                    if (comp_names.has(comp_id.value(i))) indices[indices.length] = i;
+                }
+
+                return [
+                    () => CifWriter.Category.ofTable('chem_comp_bond', chem_comp_bond, indices)
+                ];
+            }
+        }
+    }
+
+    export function attachFromMmCif(model: Model): boolean {
+        if (model.sourceData.kind !== 'mmCIF') return false;
+        const { chem_comp_bond } = model.sourceData.data;
+        if (chem_comp_bond._rowCount === 0) return false;
+        model.customProperties.add(Descriptor);
+        model._staticPropertyData.__ComponentBondData__ = chem_comp_bond;
+        return true;
+    }
+
+    export class ComponentBondImpl implements ComponentBond {
+        entries: Map<string, ComponentBond.Entry> = new Map();
+
+        addEntry(id: string) {
+            let e = new Entry(id);
+            this.entries.set(id, e);
+            return e;
+        }
+    }
+
+    export class Entry implements Entry {
+        map: Map<string, Map<string, { order: number, flags: number }>> = new Map();
+
+        add(a: string, b: string, order: number, flags: number, swap = true) {
+            let e = this.map.get(a);
+            if (e !== void 0) {
+                let f = e.get(b);
+                if (f === void 0) {
+                    e.set(b, { order, flags });
+                }
+            } else {
+                let map = new Map<string, { order: number, flags: number }>();
+                map.set(b, { order, flags });
+                this.map.set(a, map);
+            }
+
+            if (swap) this.add(b, a, order, flags, false);
+        }
+
+        constructor(public id: string) {
+        }
+    }
+
+    function getChemCompBond(model: Model) {
+        return model._staticPropertyData.__ComponentBondData__ as mmCIF_Database['chem_comp_bond'];
+    }
+
+    export const PropName = '__ComponentBond__';
+    export function get(model: Model): ComponentBond | undefined {
+        if (model._staticPropertyData[PropName]) return model._staticPropertyData[PropName];
+        if (!model.customProperties.has(Descriptor)) return void 0;
+        const chem_comp_bond = getChemCompBond(model);
+
+        let compBond = new ComponentBondImpl();
+
+        const { comp_id, atom_id_1, atom_id_2, value_order, pdbx_aromatic_flag, _rowCount: rowCount } = chem_comp_bond;
+
+        let entry = compBond.addEntry(comp_id.value(0)!);
+
+        for (let i = 0; i < rowCount; i++) {
+
+            const id = comp_id.value(i)!;
+            const nameA = atom_id_1.value(i)!;
+            const nameB = atom_id_2.value(i)!;
+            const order = value_order.value(i)!;
+            const aromatic = pdbx_aromatic_flag.value(i) === 'Y';
+
+            if (entry.id !== id) {
+                entry = compBond.addEntry(id);
+            }
+
+            let flags: number = LinkType.Flag.Covalent;
+            let ord = 1;
+            if (aromatic) flags |= LinkType.Flag.Aromatic;
+            switch (order.toLowerCase()) {
+                case 'doub':
+                case 'delo':
+                    ord = 2;
+                    break;
+                case 'trip': ord = 3; break;
+                case 'quad': ord = 4; break;
+            }
+
+            entry.add(nameA, nameB, ord, flags);
+        }
+
+        model._staticPropertyData[PropName] = compBond;
+        return compBond;
+    }
+
+    function getUniqueResidueNames(s: Structure) {
+        const prop = StructureProperties.residue.label_comp_id;
+        const names = new Set<string>();
+        const loc = Element.Location();
+        for (const unit of s.units) {
+            if (!Unit.isAtomic(unit)) continue;
+            const residues = Segmentation.transientSegments(unit.model.atomicHierarchy.residueSegments, unit.elements);
+            loc.unit = unit;
+            while (residues.hasNext) {
+                const seg = residues.move();
+                loc.element = seg.start;
+                names.add(prop(loc));
+            }
+        }
+        return names;
+    }
+}

+ 192 - 0
src/mol-model/structure/model/formats/mmcif/bonds/struct_conn.ts

@@ -0,0 +1,192 @@
+/**
+ * Copyright (c) 2017-2018 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>
+ */
+
+import Model from '../../../model'
+import { Element } from '../../../../structure'
+import { LinkType } from '../../../types'
+import { findEntityIdByAsymId, findAtomIndexByLabelName } from '../util'
+import { Column } from 'mol-data/db'
+
+export interface StructConn {
+    getResidueEntries(residueAIndex: number, residueBIndex: number): ReadonlyArray<StructConn.Entry>
+    getAtomEntries(atomIndex: number): ReadonlyArray<StructConn.Entry>
+}
+
+export namespace StructConn {
+    function _resKey(rA: number, rB: number) {
+        if (rA < rB) return `${rA}-${rB}`;
+        return `${rB}-${rA}`;
+    }
+    const _emptyEntry: Entry[] = [];
+
+    class StructConnImpl implements StructConn {
+        private _residuePairIndex: Map<string, StructConn.Entry[]> | undefined = void 0;
+        private _atomIndex: Map<number, StructConn.Entry[]> | undefined = void 0;
+
+        private getResiduePairIndex() {
+            if (this._residuePairIndex) return this._residuePairIndex;
+            this._residuePairIndex = new Map();
+            for (const e of this.entries) {
+                const ps = e.partners;
+                const l = ps.length;
+                for (let i = 0; i < l - 1; i++) {
+                    for (let j = i + i; j < l; j++) {
+                        const key = _resKey(ps[i].residueIndex, ps[j].residueIndex);
+                        if (this._residuePairIndex.has(key)) {
+                            this._residuePairIndex.get(key)!.push(e);
+                        } else {
+                            this._residuePairIndex.set(key, [e]);
+                        }
+                    }
+                }
+            }
+            return this._residuePairIndex;
+        }
+
+        private getAtomIndex() {
+            if (this._atomIndex) return this._atomIndex;
+            this._atomIndex = new Map();
+            for (const e of this.entries) {
+                for (const p of e.partners) {
+                    const key = p.atomIndex;
+                    if (this._atomIndex.has(key)) {
+                        this._atomIndex.get(key)!.push(e);
+                    } else {
+                        this._atomIndex.set(key, [e]);
+                    }
+                }
+            }
+            return this._atomIndex;
+        }
+
+
+        getResidueEntries(residueAIndex: number, residueBIndex: number): ReadonlyArray<StructConn.Entry> {
+            return this.getResiduePairIndex().get(_resKey(residueAIndex, residueBIndex)) || _emptyEntry;
+        }
+
+        getAtomEntries(atomIndex: number): ReadonlyArray<StructConn.Entry> {
+            return this.getAtomIndex().get(atomIndex) || _emptyEntry;
+        }
+
+        constructor(public entries: StructConn.Entry[]) {
+        }
+    }
+
+    export interface Entry {
+        distance: number,
+        order: number,
+        flags: number,
+        partners: { residueIndex: number, atomIndex: Element, symmetry: string }[]
+    }
+
+    type StructConnType =
+        | 'covale'
+        | 'covale_base'
+        | 'covale_phosphate'
+        | 'covale_sugar'
+        | 'disulf'
+        | 'hydrog'
+        | 'metalc'
+        | 'mismat'
+        | 'modres'
+        | 'saltbr'
+
+    export const PropName = '__StructConn__';
+    export function fromModel(model: Model): StructConn | undefined {
+        if (model._staticPropertyData[PropName]) return model._staticPropertyData[PropName];
+
+        if (model.sourceData.kind !== 'mmCIF') return;
+        const { struct_conn } = model.sourceData.data;
+        if (!struct_conn._rowCount) return void 0;
+
+        const { conn_type_id, pdbx_dist_value, pdbx_value_order } = struct_conn;
+        const p1 = {
+            label_asym_id: struct_conn.ptnr1_label_asym_id,
+            label_comp_id: struct_conn.ptnr1_label_comp_id,
+            label_seq_id: struct_conn.ptnr1_label_seq_id,
+            label_atom_id: struct_conn.ptnr1_label_atom_id,
+            label_alt_id: struct_conn.pdbx_ptnr1_label_alt_id,
+            ins_code: struct_conn.pdbx_ptnr1_PDB_ins_code,
+            symmetry: struct_conn.ptnr1_symmetry
+        };
+        const p2: typeof p1 = {
+            label_asym_id: struct_conn.ptnr2_label_asym_id,
+            label_comp_id: struct_conn.ptnr2_label_comp_id,
+            label_seq_id: struct_conn.ptnr2_label_seq_id,
+            label_atom_id: struct_conn.ptnr2_label_atom_id,
+            label_alt_id: struct_conn.pdbx_ptnr2_label_alt_id,
+            ins_code: struct_conn.pdbx_ptnr2_PDB_ins_code,
+            symmetry: struct_conn.ptnr2_symmetry
+        };
+
+        const _p = (row: number, ps: typeof p1) => {
+            if (ps.label_asym_id.valueKind(row) !== Column.ValueKind.Present) return void 0;
+            const asymId = ps.label_asym_id.value(row)
+            const residueIndex = model.atomicHierarchy.findResidueKey(
+                findEntityIdByAsymId(model, asymId),
+                ps.label_comp_id.value(row),
+                asymId,
+                ps.label_seq_id.value(row),
+                ps.ins_code.value(row)
+            );
+            if (residueIndex < 0) return void 0;
+            const atomName = ps.label_atom_id.value(row);
+            // turns out "mismat" records might not have atom name value
+            if (!atomName) return void 0;
+            const atomIndex = findAtomIndexByLabelName(model, residueIndex, atomName, ps.label_alt_id.value(row));
+            if (atomIndex < 0) return void 0;
+            return { residueIndex, atomIndex, symmetry: ps.symmetry.value(row) || '1_555' };
+        }
+
+        const _ps = (row: number) => {
+            const ret = [];
+            let p = _p(row, p1);
+            if (p) ret.push(p);
+            p = _p(row, p2);
+            if (p) ret.push(p);
+            return ret;
+        }
+
+        const entries: StructConn.Entry[] = [];
+        for (let i = 0; i < struct_conn._rowCount; i++) {
+            const partners = _ps(i);
+            if (partners.length < 2) continue;
+
+            const type = conn_type_id.value(i)! as StructConnType;
+            const orderType = (pdbx_value_order.value(i) || '').toLowerCase();
+            let flags = LinkType.Flag.None;
+            let order = 1;
+
+            switch (orderType) {
+                case 'sing': order = 1; break;
+                case 'doub': order = 2; break;
+                case 'trip': order = 3; break;
+                case 'quad': order = 4; break;
+            }
+
+            switch (type) {
+                case 'covale':
+                case 'covale_base':
+                case 'covale_phosphate':
+                case 'covale_sugar':
+                case 'modres':
+                    flags = LinkType.Flag.Covalent;
+                    break;
+                case 'disulf': flags = LinkType.Flag.Covalent | LinkType.Flag.Sulfide; break;
+                case 'hydrog': flags = LinkType.Flag.Hydrogen; break;
+                case 'metalc': flags = LinkType.Flag.MetallicCoordination; break;
+                case 'saltbr': flags = LinkType.Flag.Ion; break;
+            }
+
+            entries.push({ flags, order, distance: pdbx_dist_value.value(i), partners });
+        }
+
+        const ret = new StructConnImpl(entries);
+        model._staticPropertyData[PropName] = ret;
+        return ret;
+    }
+}

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

@@ -5,6 +5,7 @@
  */
 
 import Model from '../../model'
+import { Element } from '../../../structure'
 
 export function findEntityIdByAsymId(model: Model, asymId: string) {
     if (model.sourceData.kind !== 'mmCIF') return ''
@@ -15,12 +16,12 @@ export function findEntityIdByAsymId(model: Model, asymId: string) {
     return ''
 }
 
-export function findAtomIndexByLabelName(model: Model, residueIndex: number, atomName: string, altLoc: string | null) {
+export function findAtomIndexByLabelName(model: Model, residueIndex: number, atomName: string, altLoc: string | null): Element {
     const { segmentMap, segments } = model.atomicHierarchy.residueSegments
     const idx = segmentMap[residueIndex]
     const { label_atom_id, label_alt_id } = model.atomicHierarchy.atoms;
     for (let i = segments[idx], n = segments[idx + 1]; i <= n; ++i) {
-        if (label_atom_id.value(i) === atomName && (!altLoc || label_alt_id.value(i) === altLoc)) return i;
+        if (label_atom_id.value(i) === atomName && (!altLoc || label_alt_id.value(i) === altLoc)) return i as Element;
     }
-    return -1;
+    return -1 as Element;
 }

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

@@ -11,6 +11,7 @@ import { AtomicHierarchy, AtomicConformation } from './properties/atomic'
 import { ModelSymmetry } from './properties/symmetry'
 import { CoarseHierarchy, CoarseConformation } from './properties/coarse'
 import { Entities } from './properties/common';
+import { CustomProperties } from './properties/custom';
 import { SecondaryStructure } from './properties/seconday-structure';
 
 // import from_gro from './formats/gro'
@@ -35,16 +36,22 @@ interface Model extends Readonly<{
     atomicHierarchy: AtomicHierarchy,
     atomicConformation: AtomicConformation,
 
-    /** Various parts of the code can "cache" custom properties here */
     properties: {
+        // secondary structure provided by the input file
         readonly secondaryStructure: SecondaryStructure,
         // maps modified residue name to its parent
         readonly modifiedResidueNameMap: Map<string, string>,
-        readonly asymIdSerialMap: Map<string, number>,
-        [customName: string]: any
+        readonly asymIdSerialMap: Map<string, number>
     },
 
-    // TODO: separate properties to "static" (propagated with trajectory) and "dynamic" (computed for each frame separately)
+    customProperties: CustomProperties,
+
+    /**
+     * Not to be accessed directly, each custom property descriptor
+     * defines property accessors that use this field to store the data.
+     */
+    _staticPropertyData: { [name: string]: any },
+    _dynamicPropertyData: { [name: string]: any },
 
     coarseHierarchy: CoarseHierarchy,
     coarseConformation: CoarseConformation

+ 8 - 0
src/mol-model/structure/model/properties/custom.ts

@@ -0,0 +1,8 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+export * from './custom/descriptor'
+export * from './custom/collection'

+ 25 - 0
src/mol-model/structure/model/properties/custom/collection.ts

@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { ModelPropertyDescriptor } from './descriptor'
+
+export class CustomProperties {
+    private _list: ModelPropertyDescriptor[] = [];
+    private _set = new Set<ModelPropertyDescriptor>();
+
+    get all(): ReadonlyArray<ModelPropertyDescriptor> {
+        return this._list;
+    }
+
+    add(desc: ModelPropertyDescriptor) {
+        this._list.push(desc);
+        this._set.add(desc);
+    }
+
+    has(desc: ModelPropertyDescriptor): boolean {
+        return this._set.has(desc);
+    }
+}

+ 21 - 0
src/mol-model/structure/model/properties/custom/descriptor.ts

@@ -0,0 +1,21 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { CifWriter } from 'mol-io/writer/cif'
+import { CifExportContext } from '../../../export/mmcif';
+
+interface ModelPropertyDescriptor {
+    readonly isStatic: boolean,
+    readonly name: string,
+
+    cifExport: {
+        /** used category names that can be used for "filtering" by the writer */
+        readonly categoryNames: ReadonlyArray<string>,
+        categoryProvider: (ctx: CifExportContext) => CifWriter.Category.Provider[]
+    }
+}
+
+export { ModelPropertyDescriptor }

+ 0 - 2
src/mol-model/structure/query.ts

@@ -8,13 +8,11 @@ import Selection from './query/selection'
 import Query from './query/query'
 import * as generators from './query/generators'
 import * as modifiers from './query/modifiers'
-import props from './query/properties'
 import pred from './query/predicates'
 
 export const Queries = {
     generators,
     modifiers,
-    props,
     pred
 }
 

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

@@ -6,8 +6,7 @@
 
 import Query from './query'
 import Selection from './selection'
-import P from './properties'
-import { Element, Unit } from '../structure'
+import { Element, Unit, StructureProperties as P } from '../structure'
 import { OrderedSet, Segmentation } from 'mol-data/int'
 import { LinearGroupingBuilder } from './utils/builders';
 

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

@@ -4,8 +4,7 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { Element } from '../structure'
-import P from './properties'
+import { Element, StructureProperties as P } from '../structure'
 
 namespace Predicates {
     export interface SetLike<A> { has(v: A): boolean }

+ 1 - 117
src/mol-model/structure/query/properties.ts

@@ -4,120 +4,4 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { Element, Unit } from '../structure'
-import { VdwRadius } from '../model/properties/atomic';
-
-const constant = {
-    true: Element.property(l => true),
-    false: Element.property(l => false),
-    zero: Element.property(l => 0)
-}
-
-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),
-
-    // Conformation
-    x: Element.property(l => l.unit.conformation.x(l.element)),
-    y: Element.property(l => l.unit.conformation.y(l.element)),
-    z: Element.property(l => l.unit.conformation.z(l.element)),
-    id: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicConformation.atomId.value(l.element)),
-    occupancy: Element.property(l => !Unit.isAtomic(l.unit) ?  notAtomic() : l.unit.model.atomicConformation.occupancy.value(l.element)),
-    B_iso_or_equiv: Element.property(l => !Unit.isAtomic(l.unit) ?  notAtomic() : l.unit.model.atomicConformation.B_iso_or_equiv.value(l.element)),
-
-    // Hierarchy
-    type_symbol: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.atoms.type_symbol.value(l.element)),
-    label_atom_id: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.atoms.label_atom_id.value(l.element)),
-    auth_atom_id: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.atoms.auth_atom_id.value(l.element)),
-    label_alt_id: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.atoms.label_alt_id.value(l.element)),
-    pdbx_formal_charge: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.atoms.pdbx_formal_charge.value(l.element)),
-
-    // Derived
-    vdw_radius: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : VdwRadius(l.unit.model.atomicHierarchy.atoms.type_symbol.value(l.element))),
-}
-
-const residue = {
-    key: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.residueKey[l.unit.residueIndex[l.element]]),
-
-    group_PDB: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.residues.group_PDB.value(l.unit.residueIndex[l.element])),
-    label_comp_id: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.residues.label_comp_id.value(l.unit.residueIndex[l.element])),
-    auth_comp_id: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.residues.auth_comp_id.value(l.unit.residueIndex[l.element])),
-    label_seq_id: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.residues.label_seq_id.value(l.unit.residueIndex[l.element])),
-    auth_seq_id: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.residues.auth_seq_id.value(l.unit.residueIndex[l.element])),
-    pdbx_PDB_ins_code: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.residues.pdbx_PDB_ins_code.value(l.unit.residueIndex[l.element])),
-
-    // Properties
-    secondary_structure_type: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.properties.secondaryStructure.type[l.unit.residueIndex[l.element]]),
-    secondary_structure_key: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.properties.secondaryStructure.key[l.unit.residueIndex[l.element]]),
-}
-
-const chain = {
-    key: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.chainKey[l.unit.chainIndex[l.element]]),
-
-    label_asym_id: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.chains.label_asym_id.value(l.unit.chainIndex[l.element])),
-    auth_asym_id: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.chains.auth_asym_id.value(l.unit.chainIndex[l.element])),
-    label_entity_id: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.chains.label_entity_id.value(l.unit.chainIndex[l.element]))
-}
-
-const coarse = {
-    key: atom.key,
-    modelKey: Element.property(l => !Unit.isCoarse(l.unit) ? notCoarse() : l.unit.coarseElements.modelKey[l.element]),
-    entityKey: Element.property(l => !Unit.isCoarse(l.unit) ? notCoarse() : l.unit.coarseElements.entityKey[l.element]),
-
-    x: atom.x,
-    y: atom.y,
-    z: atom.z,
-
-    asym_id: Element.property(l => !Unit.isCoarse(l.unit) ? notCoarse() : l.unit.coarseElements.asym_id.value(l.element)),
-    seq_id_begin: Element.property(l => !Unit.isCoarse(l.unit) ? notCoarse() : l.unit.coarseElements.seq_id_begin.value(l.element)),
-    seq_id_end: Element.property(l => !Unit.isCoarse(l.unit) ? notCoarse() : l.unit.coarseElements.seq_id_end.value(l.element)),
-
-    sphere_radius: Element.property(l => !Unit.isSpheres(l.unit) ? notCoarse('spheres') : l.unit.coarseConformation.radius[l.element]),
-    sphere_rmsf: Element.property(l => !Unit.isSpheres(l.unit) ? notCoarse('spheres') : l.unit.coarseConformation.rmsf[l.element]),
-
-    gaussian_weight: Element.property(l => !Unit.isGaussians(l.unit) ? notCoarse('gaussians') : l.unit.coarseConformation.weight[l.element]),
-    gaussian_covariance_matrix: Element.property(l => !Unit.isGaussians(l.unit) ? notCoarse('gaussians') : l.unit.coarseConformation.covariance_matrix[l.element])
-}
-
-function eK(l: Element.Location) { return !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.entityKey[l.unit.chainIndex[l.element]]; }
-
-const entity = {
-    key: eK,
-
-    id: Element.property(l => l.unit.model.entities.data.id.value(eK(l))),
-    type: Element.property(l => l.unit.model.entities.data.type.value(eK(l))),
-    src_method: Element.property(l => l.unit.model.entities.data.src_method.value(eK(l))),
-    pdbx_description: Element.property(l => l.unit.model.entities.data.pdbx_description.value(eK(l))),
-    formula_weight: Element.property(l => l.unit.model.entities.data.formula_weight.value(eK(l))),
-    pdbx_number_of_molecules: Element.property(l => l.unit.model.entities.data.pdbx_number_of_molecules.value(eK(l))),
-    details: Element.property(l => l.unit.model.entities.data.details.value(eK(l))),
-    pdbx_mutation: Element.property(l => l.unit.model.entities.data.pdbx_mutation.value(eK(l))),
-    pdbx_fragment: Element.property(l => l.unit.model.entities.data.pdbx_fragment.value(eK(l))),
-    pdbx_ec: Element.property(l => l.unit.model.entities.data.pdbx_ec.value(eK(l)))
-}
-
-const unit = {
-    operator_name: Element.property(l => l.unit.conformation.operator.name),
-    model_num: Element.property(l => l.unit.model.modelNum)
-}
-
-const Properties = {
-    constant,
-    atom,
-    residue,
-    chain,
-    entity,
-    unit,
-    coarse
-}
-
-type Properties = typeof Properties
-export default Properties
+// TODO

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

@@ -9,5 +9,6 @@ import Structure from './structure/structure'
 import Unit from './structure/unit'
 import StructureSymmetry from './structure/symmetry'
 import { Link } from './structure/unit/links'
+import StructureProperties from './structure/properties'
 
-export { Element, Link, Structure, Unit, StructureSymmetry }
+export { Element, Link, Structure, Unit, StructureSymmetry, StructureProperties }

+ 126 - 0
src/mol-model/structure/structure/properties.ts

@@ -0,0 +1,126 @@
+/**
+ * Copyright (c) 2017-2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import Element from './element'
+import Unit from './unit'
+import { VdwRadius } from '../model/properties/atomic';
+
+const constant = {
+    true: Element.property(l => true),
+    false: Element.property(l => false),
+    zero: Element.property(l => 0)
+}
+
+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.`;
+}
+
+// TODO: remove the type checks?
+
+const atom = {
+    key: Element.property(l => l.element),
+
+    // Conformation
+    x: Element.property(l => l.unit.conformation.x(l.element)),
+    y: Element.property(l => l.unit.conformation.y(l.element)),
+    z: Element.property(l => l.unit.conformation.z(l.element)),
+    id: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicConformation.atomId.value(l.element)),
+    occupancy: Element.property(l => !Unit.isAtomic(l.unit) ?  notAtomic() : l.unit.model.atomicConformation.occupancy.value(l.element)),
+    B_iso_or_equiv: Element.property(l => !Unit.isAtomic(l.unit) ?  notAtomic() : l.unit.model.atomicConformation.B_iso_or_equiv.value(l.element)),
+
+    // Hierarchy
+    type_symbol: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.atoms.type_symbol.value(l.element)),
+    label_atom_id: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.atoms.label_atom_id.value(l.element)),
+    auth_atom_id: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.atoms.auth_atom_id.value(l.element)),
+    label_alt_id: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.atoms.label_alt_id.value(l.element)),
+    pdbx_formal_charge: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.atoms.pdbx_formal_charge.value(l.element)),
+
+    // Derived
+    vdw_radius: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : VdwRadius(l.unit.model.atomicHierarchy.atoms.type_symbol.value(l.element))),
+}
+
+const residue = {
+    key: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.residueKey[l.unit.residueIndex[l.element]]),
+
+    group_PDB: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.residues.group_PDB.value(l.unit.residueIndex[l.element])),
+    label_comp_id: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.residues.label_comp_id.value(l.unit.residueIndex[l.element])),
+    auth_comp_id: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.residues.auth_comp_id.value(l.unit.residueIndex[l.element])),
+    label_seq_id: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.residues.label_seq_id.value(l.unit.residueIndex[l.element])),
+    auth_seq_id: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.residues.auth_seq_id.value(l.unit.residueIndex[l.element])),
+    pdbx_PDB_ins_code: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.residues.pdbx_PDB_ins_code.value(l.unit.residueIndex[l.element])),
+
+    // Properties
+    secondary_structure_type: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.properties.secondaryStructure.type[l.unit.residueIndex[l.element]]),
+    secondary_structure_key: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.properties.secondaryStructure.key[l.unit.residueIndex[l.element]]),
+}
+
+const chain = {
+    key: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.chainKey[l.unit.chainIndex[l.element]]),
+
+    label_asym_id: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.chains.label_asym_id.value(l.unit.chainIndex[l.element])),
+    auth_asym_id: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.chains.auth_asym_id.value(l.unit.chainIndex[l.element])),
+    label_entity_id: Element.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.chains.label_entity_id.value(l.unit.chainIndex[l.element]))
+}
+
+const coarse = {
+    key: atom.key,
+    modelKey: Element.property(l => !Unit.isCoarse(l.unit) ? notCoarse() : l.unit.coarseElements.modelKey[l.element]),
+    entityKey: Element.property(l => !Unit.isCoarse(l.unit) ? notCoarse() : l.unit.coarseElements.entityKey[l.element]),
+
+    x: atom.x,
+    y: atom.y,
+    z: atom.z,
+
+    asym_id: Element.property(l => !Unit.isCoarse(l.unit) ? notCoarse() : l.unit.coarseElements.asym_id.value(l.element)),
+    seq_id_begin: Element.property(l => !Unit.isCoarse(l.unit) ? notCoarse() : l.unit.coarseElements.seq_id_begin.value(l.element)),
+    seq_id_end: Element.property(l => !Unit.isCoarse(l.unit) ? notCoarse() : l.unit.coarseElements.seq_id_end.value(l.element)),
+
+    sphere_radius: Element.property(l => !Unit.isSpheres(l.unit) ? notCoarse('spheres') : l.unit.coarseConformation.radius[l.element]),
+    sphere_rmsf: Element.property(l => !Unit.isSpheres(l.unit) ? notCoarse('spheres') : l.unit.coarseConformation.rmsf[l.element]),
+
+    gaussian_weight: Element.property(l => !Unit.isGaussians(l.unit) ? notCoarse('gaussians') : l.unit.coarseConformation.weight[l.element]),
+    gaussian_covariance_matrix: Element.property(l => !Unit.isGaussians(l.unit) ? notCoarse('gaussians') : l.unit.coarseConformation.covariance_matrix[l.element])
+}
+
+function eK(l: Element.Location) { return !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.entityKey[l.unit.chainIndex[l.element]]; }
+
+const entity = {
+    key: eK,
+
+    id: Element.property(l => l.unit.model.entities.data.id.value(eK(l))),
+    type: Element.property(l => l.unit.model.entities.data.type.value(eK(l))),
+    src_method: Element.property(l => l.unit.model.entities.data.src_method.value(eK(l))),
+    pdbx_description: Element.property(l => l.unit.model.entities.data.pdbx_description.value(eK(l))),
+    formula_weight: Element.property(l => l.unit.model.entities.data.formula_weight.value(eK(l))),
+    pdbx_number_of_molecules: Element.property(l => l.unit.model.entities.data.pdbx_number_of_molecules.value(eK(l))),
+    details: Element.property(l => l.unit.model.entities.data.details.value(eK(l))),
+    pdbx_mutation: Element.property(l => l.unit.model.entities.data.pdbx_mutation.value(eK(l))),
+    pdbx_fragment: Element.property(l => l.unit.model.entities.data.pdbx_fragment.value(eK(l))),
+    pdbx_ec: Element.property(l => l.unit.model.entities.data.pdbx_ec.value(eK(l)))
+}
+
+const unit = {
+    operator_name: Element.property(l => l.unit.conformation.operator.name),
+    model_num: Element.property(l => l.unit.model.modelNum)
+}
+
+const StructureProperties = {
+    constant,
+    atom,
+    residue,
+    chain,
+    entity,
+    unit,
+    coarse
+}
+
+type StructureProperties = typeof StructureProperties
+export default StructureProperties

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

@@ -14,9 +14,10 @@ import Unit from './unit'
 import { StructureLookup3D } from './util/lookup3d';
 import { CoarseElements } from '../model/properties/coarse';
 import { StructureSubsetBuilder } from './util/subset-builder';
-import { Queries } from '../query';
 import { InterUnitBonds, computeInterUnitBonds } from './unit/links';
 import { CrossLinkRestraints, extractCrossLinkRestraints } from './unit/pair-restraints';
+import StructureSymmetry from './symmetry';
+import StructureProperties from './properties';
 
 class Structure {
     readonly unitMap: IntMap<Unit>;
@@ -77,6 +78,13 @@ class Structure {
         return this._crossLinkRestraints;
     }
 
+    private _unitSymmetryGroups?: ReadonlyArray<Unit.SymmetryGroup> = void 0;
+    get unitSymmetryGroups(): ReadonlyArray<Unit.SymmetryGroup> {
+        if (this._unitSymmetryGroups) return this._unitSymmetryGroups;
+        this._unitSymmetryGroups = StructureSymmetry.computeTransformGroups(this);
+        return this._unitSymmetryGroups;
+    }
+
     constructor(units: ArrayLike<Unit>) {
         const map = IntMap.Mutable<Unit>();
         let elementCount = 0;
@@ -253,7 +261,7 @@ namespace Structure {
         const keys = UniqueArray.create<number, number>();
 
         for (const unit of units) {
-            const prop = unit.kind === Unit.Kind.Atomic ? Queries.props.entity.key : Queries.props.coarse.entityKey;
+            const prop = unit.kind === Unit.Kind.Atomic ? StructureProperties.entity.key : StructureProperties.coarse.entityKey;
 
             l.unit = unit;
             const elements = unit.elements;

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

@@ -68,7 +68,7 @@ namespace StructureSymmetry {
         return EquivalenceClasses<number, Unit>(hashUnit, areUnitsEquivalent);
     }
 
-    export function getTransformGroups(s: Structure): ReadonlyArray<Unit.SymmetryGroup> {
+    export function computeTransformGroups(s: Structure): ReadonlyArray<Unit.SymmetryGroup> {
         const groups = UnitEquivalenceBuilder();
         for (const u of s.units) groups.add(u.id, u);
 

+ 14 - 0
src/mol-model/structure/structure/unit/links/inter-compute.ts

@@ -11,6 +11,7 @@ import Unit from '../../unit';
 import { getElementIdx, getElementPairThreshold, getElementThreshold, isHydrogen, LinkComputationParameters, MetalsSet } from './common';
 import { InterUnitBonds } from './data';
 import { UniqueArray } from 'mol-data/generic';
+import { SortedArray } from 'mol-data/int';
 
 const MAX_RADIUS = 4;
 
@@ -57,6 +58,19 @@ function findPairLinks(unitA: Unit.Atomic, unitB: Unit.Atomic, params: LinkCompu
         const metalA = MetalsSet.has(aeI);
         const structConnEntries = params.forceCompute ? void 0 : structConn && structConn.getAtomEntries(aI);
 
+        if (structConnEntries) {
+            for (const se of structConnEntries) {
+                if (se.distance < MAX_RADIUS) continue;
+
+                for (const p of se.partners) {
+                    const _bI = SortedArray.indexOf(unitB.elements, p.atomIndex);
+                    if (_bI < 0) continue;
+                    addLink(_aI, _bI, se.order, se.flags, state);
+                    bondCount++;
+                }
+            }
+        }
+
         for (let ni = 0; ni < count; ni++) {
             const _bI = indices[ni];
             const bI = atomsB[_bI];

+ 17 - 1
src/mol-model/structure/structure/unit/links/intra-compute.ts

@@ -10,6 +10,7 @@ import { StructConn, ComponentBond } from '../../../model/formats/mmcif/bonds'
 import Unit from '../../unit'
 import { IntAdjacencyGraph } from 'mol-math/graph';
 import { LinkComputationParameters, getElementIdx, MetalsSet, getElementThreshold, isHydrogen, getElementPairThreshold } from './common';
+import { SortedArray } from 'mol-data/int';
 
 function getGraph(atomA: number[], atomB: number[], _order: number[], _flags: number[], atomCount: number): IntraUnitLinks {
     const builder = new IntAdjacencyGraph.EdgeBuilder(atomCount, atomA, atomB);
@@ -35,7 +36,7 @@ function _computeBonds(unit: Unit.Atomic, params: LinkComputationParameters): In
     const query3d = unit.lookup3d;
 
     const structConn = unit.model.sourceData.kind === 'mmCIF' ? StructConn.fromModel(unit.model) : void 0;
-    const component = unit.model.sourceData.kind === 'mmCIF' ? ComponentBond.fromModel(unit.model) : void 0;
+    const component = unit.model.sourceData.kind === 'mmCIF' ? ComponentBond.get(unit.model) : void 0;
 
     const atomA: number[] = [];
     const atomB: number[] = [];
@@ -70,6 +71,21 @@ function _computeBonds(unit: Unit.Atomic, params: LinkComputationParameters): In
         const metalA = MetalsSet.has(aeI);
         const structConnEntries = params.forceCompute ? void 0 : structConn && structConn.getAtomEntries(aI);
 
+        if (structConnEntries) {
+            for (const se of structConnEntries) {
+                if (se.distance < MAX_RADIUS) continue;
+
+                for (const p of se.partners) {
+                    const _bI = SortedArray.indexOf(unit.elements, p.atomIndex);
+                    if (_bI < 0) continue;
+                    atomA[atomA.length] = _aI;
+                    atomB[atomB.length] = _bI;
+                    flags[flags.length] = se.flags;
+                    order[order.length] = se.order;
+                }
+            }
+        }
+
         for (let ni = 0; ni < count; ni++) {
             const _bI = indices[ni];
             const bI = atoms[_bI];

+ 9 - 9
src/mol-view/label.ts

@@ -5,7 +5,7 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { Unit, Element, Queries } from 'mol-model/structure';
+import { Unit, Element, StructureProperties as Props } from 'mol-model/structure';
 import { Loci } from 'mol-model/loci';
 import { OrderedSet } from 'mol-data/int';
 
@@ -49,17 +49,17 @@ export function elementLabel(loc: Element.Location) {
     let element = ''
 
     if (Unit.isAtomic(loc.unit)) {
-        const asym_id = Queries.props.chain.auth_asym_id(loc)
-        const seq_id = Queries.props.residue.auth_seq_id(loc)
-        const comp_id = Queries.props.residue.auth_comp_id(loc)
-        const atom_id = Queries.props.atom.auth_atom_id(loc)
+        const asym_id = Props.chain.auth_asym_id(loc)
+        const seq_id = Props.residue.auth_seq_id(loc)
+        const comp_id = Props.residue.auth_comp_id(loc)
+        const atom_id = Props.atom.auth_atom_id(loc)
         element = `[${comp_id}]${seq_id}:${asym_id}.${atom_id}`
     } else if (Unit.isCoarse(loc.unit)) {
-        const asym_id = Queries.props.coarse.asym_id(loc)
-        const seq_id_begin = Queries.props.coarse.seq_id_begin(loc)
-        const seq_id_end = Queries.props.coarse.seq_id_end(loc)
+        const asym_id = Props.coarse.asym_id(loc)
+        const seq_id_begin = Props.coarse.seq_id_begin(loc)
+        const seq_id_end = Props.coarse.seq_id_end(loc)
         if (seq_id_begin === seq_id_end) {
-            const entityKey = Queries.props.coarse.entityKey(loc)
+            const entityKey = Props.coarse.entityKey(loc)
             const seq = loc.unit.model.sequence.byEntityKey[entityKey]
             const comp_id = seq.compId.value(seq_id_begin)
             element = `[${comp_id}]${seq_id_begin}:${asym_id}`

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

@@ -11,7 +11,7 @@ import * as fs from 'fs'
 import fetch from 'node-fetch'
 import CIF from 'mol-io/reader/cif'
 
-import { Structure, Model, Queries as Q, Element, Selection, StructureSymmetry, Query, Format } from 'mol-model/structure'
+import { Structure, Model, Queries as Q, Element, Selection, StructureSymmetry, Query, Format, StructureProperties as SP } from 'mol-model/structure'
 //import { Segmentation, OrderedSet } from 'mol-data/int'
 
 import to_mmCIF from 'mol-model/structure/export/mmcif'
@@ -293,7 +293,7 @@ export namespace PropertyAccess {
     export async function testAssembly(id: string, s: Structure) {
         console.time('assembly')
         const a = await StructureSymmetry.buildAssembly(s, '1').run();
-        //const auth_comp_id = Q.props.residue.auth_comp_id;
+        //const auth_comp_id = SP.residue.auth_comp_id;
         //const q1 = Query(Q.generators.atoms({ residueTest: l => auth_comp_id(l) === 'ALA' }));
         //const alas = await query(q1, a);
 
@@ -306,7 +306,7 @@ export namespace PropertyAccess {
     export async function testSymmetry(id: string, s: Structure) {
         console.time('symmetry')
         const a = await StructureSymmetry.buildSymmetryRange(s, Vec3.create(-1, -1, -1), Vec3.create(1, 1, 1)).run();
-        //const auth_comp_id = Q.props.residue.auth_comp_id;
+        //const auth_comp_id = SP.residue.auth_comp_id;
         //const q1 = Query(Q.generators.atoms({ residueTest: l => auth_comp_id(l) === 'ALA' }));
         //const alas = await query(q1, a);
 
@@ -322,7 +322,7 @@ export namespace PropertyAccess {
         const a = await StructureSymmetry.buildSymmetryRange(s, Vec3.create(-2, -2, -2), Vec3.create(2, 2, 2)).run();
         //console.log(printUnits(a));
 
-        const auth_comp_id = Q.props.residue.auth_comp_id, op = Q.props.unit.operator_name;
+        const auth_comp_id = SP.residue.auth_comp_id, op = SP.unit.operator_name;
         //const q1 = Q.generators.atoms({ residueTest: l => auth_comp_id(l) === 'REA' });
         const q1 = Q.modifiers.includeSurroundings(Q.generators.atoms({
             chainTest: l => op(l) === '1_555',
@@ -345,7 +345,7 @@ export namespace PropertyAccess {
         // const it = surr.elementLocations();
         // while (it.hasNext) {
         //     const e = it.move();
-        //     console.log(`${Q.props.unit.operator_name(e)} ${Q.props.atom.id(e)}`);
+        //     console.log(`${SP.unit.operator_name(e)} ${SP.atom.id(e)}`);
         // }
     //fs.writeFileSync(`${DATA_DIR}/${id}_surr.bcif`, to_mmCIF(id, a, true));
         fs.writeFileSync(`${DATA_DIR}/${id}_surr.cif`, to_mmCIF(id, surr, false));
@@ -418,7 +418,7 @@ export namespace PropertyAccess {
         //console.log('r', sumPropertyResidue(structures[0], l => l.unit.hierarchy.residues.auth_seq_id.value(l.unit.residueIndex[l.atom])));
 
         console.time('atom.x');
-        console.log('atom.x', sumProperty(structures[0], Q.props.atom.x));
+        console.log('atom.x', sumProperty(structures[0], SP.atom.x));
         console.timeEnd('atom.x');
         console.time('__x')
         //console.log('__x', sumProperty(structures[0], l => l.unit.conformation.x[l.atom]));
@@ -426,16 +426,16 @@ export namespace PropertyAccess {
 
         //const authSeqId = Element.property(l => l.unit.hierarchy.residues.auth_seq_id.value(l.unit.residueIndex[l.atom]));
 
-        //const auth_seq_id = Q.props.residue.auth_seq_id;
-        const auth_comp_id = Q.props.residue.auth_comp_id;
-        //const auth_asym_id = Q.props.chain.auth_asym_id;
+        //const auth_seq_id = SP.residue.auth_seq_id;
+        const auth_comp_id = SP.residue.auth_comp_id;
+        //const auth_asym_id = SP.chain.auth_asym_id;
         //const set =  new Set(['A', 'B', 'C', 'D']);
         //const q = Q.generators.atomGroups({ atomTest: l => auth_seq_id(l) < 3 });
-        const q = Query(Q.generators.atoms({ atomTest: Q.pred.eq(Q.props.residue.auth_comp_id, 'ALA') }));
-        const P = Q.props
+        const q = Query(Q.generators.atoms({ atomTest: Q.pred.eq(SP.residue.auth_comp_id, 'ALA') }));
+        const P = SP
         //const q0 = Q.generators.atoms({ atomTest: l => auth_comp_id(l) === 'ALA' });
         const q1 = Query(Q.generators.atoms({ residueTest: l => auth_comp_id(l) === 'ALA' }));
-        const q2 = Query(Q.generators.atoms({ residueTest: l => auth_comp_id(l) === 'ALA', groupBy: Q.props.residue.key }));
+        const q2 = Query(Q.generators.atoms({ residueTest: l => auth_comp_id(l) === 'ALA', groupBy: SP.residue.key }));
         const q3 = Query(Q.generators.atoms({
             chainTest: Q.pred.inSet(P.chain.auth_asym_id, ['A', 'B', 'C', 'D']),
             residueTest: Q.pred.eq(P.residue.auth_comp_id, 'ALA')

+ 10 - 10
src/servers/model/server/api.ts

@@ -4,7 +4,7 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { Query, Queries, Structure, Element, StructureSymmetry } from 'mol-model/structure';
+import { Query, Queries, Structure, Element, StructureSymmetry, StructureProperties as Props } from 'mol-model/structure';
 
 export enum QueryParamType {
     String,
@@ -47,23 +47,23 @@ const AtomSiteParameters = {
 
 // function entityTest(params: any): Element.Predicate | undefined {
 //     if (typeof params.entity_id === 'undefined') return void 0;
-//     const p = Queries.props.entity.id, id = '' + params.entityId;
+//     const p = Props.entity.id, id = '' + params.entityId;
 //     return Element.property(l => p(l) === id);
 // }
 
 function entityTest1_555(params: any): Element.Predicate | undefined {
     if (typeof params.entity_id === 'undefined') return Element.property(l => l.unit.conformation.operator.isIdentity);
-    const p = Queries.props.entity.id, id = '' + params.entityId;
+    const p = Props.entity.id, id = '' + params.entityId;
     return Element.property(l => l.unit.conformation.operator.isIdentity && p(l) === id);
 }
 
 function chainTest(params: any): Element.Predicate | undefined {
     if (typeof params.label_asym_id !== 'undefined') {
-        const p = Queries.props.chain.label_asym_id, id = '' + params.label_asym_id;
+        const p = Props.chain.label_asym_id, id = '' + params.label_asym_id;
         return Element.property(l => p(l) === id);
     }
     if (typeof params.auth_asym_id !== 'undefined') {
-        const p = Queries.props.chain.auth_asym_id, id = '' + params.auth_asym_id;
+        const p = Props.chain.auth_asym_id, id = '' + params.auth_asym_id;
         return Element.property(l => p(l) === id);
     }
     return void 0;
@@ -73,27 +73,27 @@ function residueTest(params: any): Element.Predicate | undefined {
     const props: Element.Property<any>[] = [], values: any[] = [];
 
     if (typeof params.label_seq_id !== 'undefined') {
-        props.push(Queries.props.residue.label_seq_id);
+        props.push(Props.residue.label_seq_id);
         values.push(+params.label_seq_id);
     }
 
     if (typeof params.auth_seq_id !== 'undefined') {
-        props.push(Queries.props.residue.auth_seq_id);
+        props.push(Props.residue.auth_seq_id);
         values.push(+params.auth_seq_id);
     }
 
     if (typeof params.label_comp_id !== 'undefined') {
-        props.push(Queries.props.residue.label_comp_id);
+        props.push(Props.residue.label_comp_id);
         values.push(params.label_comp_id);
     }
 
     if (typeof params.auth_comp_id !== 'undefined') {
-        props.push(Queries.props.residue.auth_comp_id);
+        props.push(Props.residue.auth_comp_id);
         values.push(params.auth_comp_id);
     }
 
     if (typeof params.pdbx_PDB_ins_code !== 'undefined') {
-        props.push(Queries.props.residue.pdbx_PDB_ins_code);
+        props.push(Props.residue.pdbx_PDB_ins_code);
         values.push(params.pdbx_PDB_ins_code);
     }