Browse Source

Merge branch 'master' into mol2-2

Zepei Xu 7 years ago
parent
commit
30f72c4d85
46 changed files with 1567 additions and 879 deletions
  1. 1 1
      src/mol-data/_spec/equiv-index.spec.ts
  2. 5 0
      src/mol-data/db/column.ts
  3. 20 0
      src/mol-data/db/table.ts
  4. 2 1
      src/mol-data/int.ts
  5. 2 1
      src/mol-data/int/impl/ordered-set.ts
  6. 61 0
      src/mol-data/int/map.ts
  7. 3 3
      src/mol-data/iterator.ts
  8. 6 7
      src/mol-data/util.ts
  9. 25 0
      src/mol-data/util/array.ts
  10. 1 1
      src/mol-data/util/chunked-array.ts
  11. 5 7
      src/mol-data/util/equivalence-classes.ts
  12. 53 0
      src/mol-data/util/grouping.ts
  13. 8 10
      src/mol-data/util/hash-set.ts
  14. 1 1
      src/mol-data/util/unique-array.ts
  15. 1 1
      src/mol-io/common/binary-cif/array-encoder.ts
  16. 5 1
      src/mol-io/writer/cif/encoder.ts
  17. 12 3
      src/mol-math/geometry/symmetry-operator.ts
  18. 26 6
      src/mol-math/linear-algebra/3d.ts
  19. 91 46
      src/mol-model/structure/_spec/atom-set.spec.ts
  20. 2 1
      src/mol-model/structure/model.ts
  21. 7 0
      src/mol-model/structure/model/formats/mmcif.ts
  22. 149 0
      src/mol-model/structure/model/formats/mmcif/assembly.ts
  23. 6 4
      src/mol-model/structure/model/model.ts
  24. 56 0
      src/mol-model/structure/model/properties/symmetry.ts
  25. 0 9
      src/mol-model/structure/model/properties/transforms.ts
  26. 28 22
      src/mol-model/structure/query/generators.ts
  27. 1 1
      src/mol-model/structure/query/predicates.ts
  28. 2 0
      src/mol-model/structure/query/query.ts
  29. 65 75
      src/mol-model/structure/query/selection.ts
  30. 3 1
      src/mol-model/structure/structure.ts
  31. 71 0
      src/mol-model/structure/structure/atom/group.ts
  32. 0 0
      src/mol-model/structure/structure/atom/impl/properties.ts
  33. 17 22
      src/mol-model/structure/structure/atom/impl/set-builder.ts
  34. 448 0
      src/mol-model/structure/structure/atom/impl/set.ts
  35. 18 11
      src/mol-model/structure/structure/atom/set.ts
  36. 0 497
      src/mol-model/structure/structure/atom/set/impl.ts
  37. 26 22
      src/mol-model/structure/structure/structure.ts
  38. 44 0
      src/mol-model/structure/structure/symmetry.ts
  39. 21 16
      src/mol-model/structure/structure/unit.ts
  40. 2 0
      src/mol-ql/TODO
  41. 24 9
      src/mol-util/computation.ts
  42. 5 2
      src/mol-util/scheduler.ts
  43. 1 1
      src/perf-tests/chunked-array-vs-native.ts
  44. 192 79
      src/perf-tests/sets.ts
  45. 49 17
      src/perf-tests/structure.ts
  46. 2 1
      src/script.ts

+ 1 - 1
src/mol-data/_spec/equiv-index.spec.ts

@@ -4,7 +4,7 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import EquivalenceClasses from '../util/equivalence-classes'
+import { EquivalenceClasses } from '../util'
 
 describe('equiv-classes', () => {
     it('integer mod classes', () => {

+ 5 - 0
src/mol-data/db/column.ts

@@ -80,6 +80,10 @@ namespace Column {
         valueKind?: (row: number) => ValueKind,
     }
 
+    export function is(v: any): v is Column<any> {
+        return !!v && !!(v as Column<any>).schema && !!(v as Column<any>).value;
+    }
+
     export const enum ValueKind { Present = 0, NotPresent = 1, Unknown = 2 }
 
     export function Undefined<T extends Schema>(rowCount: number, schema: T): Column<T['T']> {
@@ -306,6 +310,7 @@ function mapToArrayImpl<T, S>(c: Column<T>, f: (v: T) => S, ctor: Column.ArrayCt
 }
 
 function areColumnsEqual(a: Column<any>, b: Column<any>) {
+    if (a === b) return true;
     if (a.rowCount !== b.rowCount || a.isDefined !== b.isDefined || a.schema.valueType !== b.schema.valueType) return false;
     if (!!a['@array'] && !!b['@array']) return areArraysEqual(a, b);
     return areValuesEqual(a, b);

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

@@ -130,6 +130,26 @@ namespace Table {
 
         return true;
     }
+
+    /** Allocate a new object with the given row values. */
+    export function getRow<S extends Schema>(table: Table<S>, index: number) {
+        const row: Row<S> = Object.create(null);
+        const { _columns: cols } = table;
+        for (let i = 0; i < cols.length; i++) {
+            const c = cols[i];
+            row[c] = table[c].value(index);
+        }
+        return row;
+    }
+
+    export function getRows<S extends Schema>(table: Table<S>) {
+        const ret: Row<S>[] = [];
+        const { _rowCount: c } = table;
+        for (let i = 0; i < c; i++) {
+            ret[i] = getRow(table, i);
+        }
+        return ret;
+    }
 }
 
 export default Table

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

@@ -10,6 +10,7 @@ import Segmentation from './int/segmentation'
 import SortedArray from './int/sorted-array'
 import Tuple from './int/tuple'
 import LinkedIndex from './int/linked-index'
+import IntMap from './int/map'
 import Iterator from './iterator'
 
-export { Interval, OrderedSet, Segmentation, SortedArray, Tuple, LinkedIndex, Iterator }
+export { Interval, OrderedSet, Segmentation, SortedArray, Tuple, LinkedIndex, IntMap, Iterator }

+ 2 - 1
src/mol-data/int/impl/ordered-set.ts

@@ -132,8 +132,8 @@ function unionII(a: I, b: I) {
     if (I.areEqual(a, b)) return a;
 
     const sizeA = I.size(a), sizeB = I.size(b);
-    if (!sizeA) return b;
     if (!sizeB) return a;
+    if (!sizeA) return b;
     const minA = I.min(a), minB = I.min(b);
     if (areRangesIntersecting(a, b)) return I.ofRange(Math.min(minA, minB), Math.max(I.max(a), I.max(b)));
     let lSize, lMin, rSize, rMin;
@@ -170,6 +170,7 @@ function intersectSI(a: S, b: I) {
     const start = I.start(r), end = I.end(r);
     const resultSize = end - start;
     if (!resultSize) return Empty;
+    if (resultSize === a.length) return a;
 
     const indices = new Int32Array(resultSize);
     let offset = 0;

+ 61 - 0
src/mol-data/int/map.ts

@@ -0,0 +1,61 @@
+/**
+ * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { iterableToArray } from '../util'
+
+// TODO: rename to "linear map" and just do key value mapping from index?
+
+/** Immutable by convention IntMap */
+interface IntMap<T> {
+    has(key: number): boolean,
+    keys(): IterableIterator<number>,
+    values(): IterableIterator<T>,
+    get(key: number): T,
+    readonly size: number
+}
+
+namespace IntMap {
+    export const Empty: IntMap<any> = new Map<number, any>();
+
+    export interface Mutable<T> extends IntMap<T> {
+        set(key: number, value: T): void;
+    }
+
+    export function keyArray<T>(map: IntMap<T>): number[] {
+        return iterableToArray(map.keys());
+    }
+
+    export function Mutable<T>(): Mutable<T> {
+        return new Map<number, T>() as Mutable<T>;
+    }
+
+    export function asImmutable<T>(map: IntMap<T>): IntMap<T> {
+        return map;
+    }
+
+    export function copy<T>(map: IntMap<T>): Mutable<T> {
+        const ret = Mutable<T>();
+        const it = map.keys();
+        while (true) {
+            const { done, value } = it.next();
+            if (done) break;
+            ret.set(value, map.get(value));
+        }
+        return ret;
+    }
+
+    export function addFrom<T>(map: Mutable<T>, src: IntMap<T>) {
+        const it = src.keys();
+        while (true) {
+            const { done, value } = it.next();
+            if (done) break;
+            map.set(value, src.get(value));
+        }
+        return map;
+    }
+}
+
+export default IntMap;

+ 3 - 3
src/mol-data/iterator.ts

@@ -107,11 +107,11 @@ namespace Iterator {
     export function map<T, R>(base: Iterator<T>, f: (v: T) => R): Iterator<R> { return new MapIteratorImpl(base, f); }
     export function filter<T>(base: Iterator<T>, p: (v: T) => boolean): Iterator<T> { return new FilterIteratorImpl(base, p); }
 
-    // f can return non-undefined falsy value to stop the iteration.
-    export function forEach<T, Ctx>(it: Iterator<T>, f: (v: T, ctx: Ctx) => boolean | void, ctx: Ctx): Ctx {
+    // Iterate until first truthy value is returned.
+    export function forEach<T, Ctx>(it: Iterator<T>, f: (v: T, ctx: Ctx) => any, ctx: Ctx): Ctx {
         while (it.hasNext) {
             const c = f(it.move(), ctx);
-            if (typeof c !== 'undefined' && !c) return ctx;
+            if (c) return ctx;
         }
         return ctx;
     }

+ 6 - 7
src/mol-data/util.ts

@@ -4,12 +4,11 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import ChunkedArray from './util/chunked-array'
-import EquivalenceClasses from './util/chunked-array'
-import HashSet from './util/hash-set'
-import UniqueArray from './util/unique-array'
-
+export * from './util/chunked-array'
+export * from './util/unique-array'
+export * from './util/hash-set'
+export * from './util/equivalence-classes'
 export * from './util/hash-functions'
 export * from './util/sort'
-
-export { ChunkedArray, EquivalenceClasses, HashSet, UniqueArray }
+export * from './util/grouping'
+export * from './util/array'

+ 25 - 0
src/mol-data/util/array.ts

@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * from https://github.com/dsehnal/CIFTools.js
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+export function arrayFind<T>(array: ArrayLike<T>, f: (v: T) => boolean): T | undefined {
+    for (let i = 0, _i = array.length; i < _i; i++) {
+        if (f(array[i])) return array[i];
+    }
+    return void 0;
+}
+
+export function iterableToArray<T>(it: IterableIterator<T>): T[] {
+    if (Array.from) return Array.from(it);
+    
+    const ret = [];
+    while (true) {
+        const { done, value } = it.next();
+        if (done) break;
+        ret[ret.length] = value;
+    }
+    return ret;
+}

+ 1 - 1
src/mol-data/util/chunked-array.ts

@@ -134,4 +134,4 @@ namespace ChunkedArray {
     }
 }
 
-export default ChunkedArray
+export { ChunkedArray }

+ 5 - 7
src/mol-data/util/equivalence-classes.ts

@@ -6,7 +6,7 @@
 
 class EquivalenceClassesImpl<K, V> {
     private id = 0;
-    private byHash: { [hash: number]: { id: number, keys: K[], value: V }[] } = Object.create(null);
+    private byHash = new Map<number, { id: number, keys: K[], value: V }[]>();
 
     readonly groups: K[][] = [];
 
@@ -19,8 +19,8 @@ class EquivalenceClassesImpl<K, V> {
 
     add(key: K, a: V) {
         const hash = this.getHash(a);
-        if (!!this.byHash[hash]) {
-            const groups = this.byHash[hash];
+        if (this.byHash.has(hash)) {
+            const groups = this.byHash.get(hash)!;
             for (let i = 0, _i = groups.length; i < _i; i++) {
                 const group = groups[i];
                 if (this.areEqual(a, group.value)) {
@@ -33,7 +33,7 @@ class EquivalenceClassesImpl<K, V> {
             return group.id;
         } else {
             const group = this.createGroup(key, a);
-            this.byHash[hash] = [group];
+            this.byHash.set(hash, [group]);
             return group.id;
         }
     }
@@ -41,8 +41,6 @@ class EquivalenceClassesImpl<K, V> {
     constructor(private getHash: (v: V) => any, private areEqual: (a: V, b: V) => boolean) { }
 }
 
-function EquivalenceClasses<K, V>(getHash: (x: V) => any, areEqual: (a: V, b: V) => boolean) {
+export function EquivalenceClasses<K, V>(getHash: (x: V) => any, areEqual: (a: V, b: V) => boolean) {
     return new EquivalenceClassesImpl<K, V>(getHash, areEqual);
 }
-
-export default EquivalenceClasses;

+ 53 - 0
src/mol-data/util/grouping.ts

@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { Column } from '../db'
+
+export interface Grouping<V, K> {
+    map: Map<K, V[]>,
+    keys: ReadonlyArray<K>,
+    groups: ReadonlyArray<ReadonlyArray<V>>
+}
+
+class GroupingImpl<K, V> {
+    readonly map = new Map<K, V[]>();
+    readonly keys: K[] = [];
+    readonly groups: V[][] = [];
+
+    add(a: V) {
+        const key = this.getKey(a) as any;
+        if (!!this.map.has(key)) {
+            const group = this.map.get(key)!;
+            group[group.length] = a;
+        } else {
+            const group = [a];
+            this.map.set(key, group);
+            this.keys[this.keys.length] = key;
+            this.groups[this.groups.length] = group;
+        }
+    }
+
+    getGrouping(): Grouping<V, K> {
+        return { keys: this.keys, groups: this.groups, map: this.map };
+    }
+
+    constructor(private getKey: (v: V) => K) { }
+}
+
+export function Grouper<V, K>(getKey: (x: V) => K) {
+    return new GroupingImpl<K, V>(getKey);
+}
+
+export function groupBy<V, K>(values: ArrayLike<V> | Column<V>, getKey: (x: V) => K) {
+    const gs = Grouper(getKey);
+    if (Column.is(values)) {
+        const v = values.value;
+        for (let i = 0, _i = values.rowCount; i < _i; i++) gs.add(v(i));
+    } else {
+        for (let i = 0, _i = values.length; i < _i; i++) gs.add(values[i]);
+    }
+    return gs.getGrouping();
+}

+ 8 - 10
src/mol-data/util/hash-set.ts

@@ -12,12 +12,12 @@ interface SetLike<T> {
 
 class HashSetImpl<T> implements SetLike<T> {
     size: number = 0;
-    private byHash: { [hash: number]: T[] } = Object.create(null);
+    private byHash = new Map<number, T[]>();
 
     add(a: T) {
         const hash = this.getHash(a);
-        if (!!this.byHash[hash]) {
-            const xs = this.byHash[hash];
+        if (this.byHash.has(hash)) {
+            const xs = this.byHash.get(hash)!;
             for (let i = 0, _i = xs.length; i < _i; i++) {
                 if (this.areEqual(a, xs[i])) return false;
             }
@@ -25,7 +25,7 @@ class HashSetImpl<T> implements SetLike<T> {
             this.size++;
             return true;
         } else {
-            this.byHash[hash] = [a];
+            this.byHash.set(hash, [a]);
             this.size++;
             return true;
         }
@@ -33,8 +33,8 @@ class HashSetImpl<T> implements SetLike<T> {
 
     has(v: T) {
         const hash = this.getHash(v);
-        if (!this.byHash[hash]) return false;
-        const xs = this.byHash[hash];
+        if (!this.byHash.has(hash)) return false;
+        const xs = this.byHash.get(hash)!;
         for (let i = 0, _i = xs.length; i < _i; i++) {
             if (this.areEqual(v, xs[i])) return true;
         }
@@ -45,8 +45,6 @@ class HashSetImpl<T> implements SetLike<T> {
 }
 // TODO: add implementations with multilevel hashing support?
 
-function HashSet<T>(getHash: (v: T) => any, areEqual: (a: T, b: T) => boolean): SetLike<T> {
+export function HashSet<T>(getHash: (v: T) => any, areEqual: (a: T, b: T) => boolean): SetLike<T> {
     return new HashSetImpl<T>(getHash, areEqual);
-}
-
-export default HashSet;
+}

+ 1 - 1
src/mol-data/util/unique-array.ts

@@ -21,4 +21,4 @@ namespace UniqueArray {
     }
 }
 
-export default UniqueArray
+export { UniqueArray }

+ 1 - 1
src/mol-io/common/binary-cif/array-encoder.ts

@@ -7,7 +7,7 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import ChunkedArray from 'mol-data/util/chunked-array'
+import { ChunkedArray } from 'mol-data/util'
 import { Encoding, EncodedData } from './encoding'
 
 export interface ArrayEncoder {

+ 5 - 1
src/mol-io/writer/cif/encoder.ts

@@ -14,7 +14,11 @@ import Encoder from '../encoder'
 // TODO: add "repeat encoding"? [[1, 2], [1, 2], [1, 2]] --- Repeat ---> [[1, 2], 3]
 // TODO: Add "higher level fields"? (i.e. generalization of repeat)
 // TODO: Add tensor field definition
-// TODO: align "data blocks" to 8 byte offsets
+// TODO: align "data blocks" to 8 byte offsets for fast typed array windows? (prolly needs some testing if this is actually the case too)
+// TODO: "parametric encoders"? Specify encoding as [{ param: 'value1', encoding1 }, { param: 'value2', encoding2 }]
+//       then the encoder can specify { param: 'value1' } and the correct encoding will be used.
+//       Use case: variable precision encoding for different fields.
+//       Perhaps implement this as parameter spaces...
 
 export const enum FieldType {
     Str, Int, Float

+ 12 - 3
src/mol-math/geometry/symmetry-operator.ts

@@ -21,15 +21,24 @@ namespace SymmetryOperator {
     export const DefaultName = '1_555'
     export const Default: SymmetryOperator = create(DefaultName, Mat4.identity());
 
-    export function create(name: string, matrix: Mat4, hkl?: number[]): SymmetryOperator {
-        const _hkl = hkl ? Vec3.create(hkl[0], hkl[1], hkl[2]) : Vec3.zero();
+    const RotationEpsilon = 0.0001;
+
+    export function create(name: string, matrix: Mat4, hkl?: Vec3): SymmetryOperator {
+        const _hkl = hkl ? Vec3.copy(Vec3.zero(), hkl) : Vec3.zero();
         if (Mat4.isIdentity(matrix)) return { name, matrix, inverse: Mat4.identity(), isIdentity: true, hkl: _hkl };
-        if (!Mat4.isRotationAndTranslation(matrix)) throw new Error('Symmetry operator must be a composition of rotation and translation.');
+        if (!Mat4.isRotationAndTranslation(matrix, RotationEpsilon)) throw new Error(`Symmetry operator (${name}) must be a composition of rotation and translation.`);
         return { name, matrix, inverse: Mat4.invert(Mat4.zero(), matrix), isIdentity: false, hkl: _hkl };
     }
 
+    // Apply the 1st and then 2nd operator. ( = second.matrix * first.matrix)
+    export function compose(first: SymmetryOperator, second: SymmetryOperator) {
+        const matrix = Mat4.mul(Mat4.zero(), second.matrix, first.matrix);
+        return create(second.name, matrix, second.hkl);
+    }
+
     export interface CoordinateMapper { (index: number, slot: Vec3): Vec3 }
     export interface ArrayMapping {
+        readonly operator: SymmetryOperator,
         readonly invariantPosition: CoordinateMapper,
         readonly position: CoordinateMapper,
         x(index: number): number,

+ 26 - 6
src/mol-math/linear-algebra/3d.ts

@@ -214,7 +214,7 @@ export namespace Mat4 {
         return mul(out, mul(out, a, b), c);
     }
 
-    export function translate(out: Mat4, a: Mat4, v: Mat4) {
+    export function translate(out: Mat4, a: Mat4, v: Vec3) {
         const x = v[0], y = v[1], z = v[2];
         let a00: number, a01: number, a02: number, a03: number,
             a10: number, a11: number, a12: number, a13: number,
@@ -243,7 +243,7 @@ export namespace Mat4 {
         return out;
     }
 
-    export function fromTranslation(out: Mat4, v: Mat4) {
+    export function fromTranslation(out: Mat4, v: Vec3) {
         out[0] = 1;
         out[1] = 0;
         out[2] = 0;
@@ -263,6 +263,13 @@ export namespace Mat4 {
         return out;
     }
 
+    export function setTranslation(out: Mat4, v: Vec3) {
+        out[12] = v[0];
+        out[13] = v[1];
+        out[14] = v[2];
+        return out;
+    }
+
     export function rotate(out: Mat4, a: Mat4, rad: number, axis: Mat4) {
         let x = axis[0], y = axis[1], z = axis[2],
             len = Math.sqrt(x * x + y * y + z * z),
@@ -432,15 +439,28 @@ export namespace Mat4 {
         return b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
     }
 
-    export function isRotationAndTranslation(a: Mat4) {
+    /**
+     * Check if the matrix has the form
+     * [ Rotation    Translation ]
+     * [ 0           1           ]
+     */
+    export function isRotationAndTranslation(a: Mat4, eps?: number) {
+        return _isRotationAndTranslation(a, typeof eps !== 'undefined' ? eps : EPSILON.Value)
+    }
+
+    function _isRotationAndTranslation(a: Mat4, eps: number) {
         const a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3],
             a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7],
             a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11],
             /* a30 = a[12], a31 = a[13], a32 = a[14],*/ a33 = a[15];
 
-        if (a33 !== 1 || a03 !== 0 || a13 !== 0 || a23 !== 0) return false;
-        const det3x3 = a00 * (a11 * a22 - a21 * a21) - a01 * (a10 * a22 - a12 * a20) + a02 * (a10 * a21 - a11 * a20);
-        if (det3x3 < 1 - EPSILON.Value || det3x3 > 1 + EPSILON.Value) return false;
+        if (a33 !== 1 || a03 !== 0 || a13 !== 0 || a23 !== 0) {
+            return false;
+        }
+        const det3x3 = a00 * (a11 * a22 - a12 * a21) - a01 * (a10 * a22 - a12 * a20) + a02 * (a10 * a21 - a11 * a20);
+        if (det3x3 < 1 - eps || det3x3 > 1 + eps) {
+            return false;
+        }
         return true;
     }
 }

+ 91 - 46
src/mol-model/structure/_spec/atom-set.spec.ts

@@ -7,6 +7,7 @@
 import { OrderedSet } from 'mol-data/int'
 import AtomSet from '../structure/atom/set'
 import Atom from '../structure/atom'
+import AtomGroup from '../structure/atom/group'
 
 describe('atom set', () => {
     const p = (i: number, j: number) => Atom.create(i, j);
@@ -21,7 +22,7 @@ describe('atom set', () => {
     }
 
     it('singleton pair', () => {
-        const set = AtomSet.create(p(10, 11));
+        const set = AtomSet.ofAtoms([p(10, 11)], AtomSet.Empty);
         expect(setToPairs(set)).toEqual([p(10, 11)]);
         expect(AtomSet.atomHas(set, p(10, 11))).toBe(true);
         expect(AtomSet.atomHas(set, p(11, 11))).toBe(false);
@@ -29,16 +30,20 @@ describe('atom set', () => {
         expect(AtomSet.atomCount(set)).toBe(1);
     });
 
-    it('singleton number', () => {
-        const set = AtomSet.create(p(10, 11));
+    it('singleton atom', () => {
+        const set = AtomSet.singleton(p(10, 11), AtomSet.Empty);
         expect(setToPairs(set)).toEqual([p(10, 11)]);
+        expect(AtomSet.atomHas(set, p(10, 11))).toBe(true);
+        expect(AtomSet.atomHas(set, p(11, 11))).toBe(false);
+        expect(AtomSet.atomGetAt(set, 0)).toBe(p(10, 11));
+        expect(AtomSet.atomCount(set)).toBe(1);
     });
 
     it('multi', () => {
-        const set = AtomSet.create({
-            1: OrderedSet.ofSortedArray([4, 6, 7]),
-            3: OrderedSet.ofRange(0, 1),
-        });
+        const gen = AtomSet.Generator();
+        gen.add(1, AtomGroup.createNew(OrderedSet.ofSortedArray([4, 6, 7])));
+        gen.add(3, AtomGroup.createNew(OrderedSet.ofRange(0, 1)));
+        const set = gen.getSet();
         const ret = [p(1, 4), p(1, 6), p(1, 7), p(3, 0), p(3, 1)];
         expect(AtomSet.atomCount(set)).toBe(ret.length);
         expect(setToPairs(set)).toEqual([p(1, 4), p(1, 6), p(1, 7), p(3, 0), p(3, 1)]);
@@ -50,18 +55,58 @@ describe('atom set', () => {
         }
     });
 
+    it('template', () => {
+        const template = AtomSet.ofAtoms([p(1, 3), p(0, 1), p(0, 6), p(0, 2)], AtomSet.Empty)
+        const gen = AtomSet.TemplateGenerator(template);
+        gen.add(0, OrderedSet.ofSortedArray([1, 2, 6]));
+        gen.add(1, OrderedSet.ofSingleton(3));
+        const set = gen.getSet();
+
+        expect(AtomSet.unitGetById(set, 0)).toBe(AtomSet.unitGetById(template, 0));
+        expect(AtomSet.unitGetById(set, 1)).toBe(AtomSet.unitGetById(template, 1));
+        expect(set).toBe(template);
+    });
+
+    it('template 1', () => {
+        const template = AtomSet.ofAtoms([p(1, 3), p(0, 1), p(0, 6), p(0, 2)], AtomSet.Empty)
+        const gen = AtomSet.TemplateGenerator(template);
+        gen.add(0, OrderedSet.ofSortedArray([1, 2, 6]));
+        gen.add(1, OrderedSet.ofSingleton(4));
+        const set = gen.getSet();
+
+        expect(AtomSet.unitGetById(set, 0)).toBe(AtomSet.unitGetById(template, 0));
+        expect(AtomSet.unitGetById(set, 1) === AtomSet.unitGetById(template, 1)).toBe(false);
+        expect(set === template).toBe(false);
+    });
+
+    it('template union', () => {
+        const template = AtomSet.ofAtoms([p(1, 3), p(0, 1), p(0, 6), p(0, 2)], AtomSet.Empty)
+
+        const p13 = AtomSet.ofAtoms([p(1, 3)], AtomSet.Empty);
+        const p01 = AtomSet.ofAtoms([p(0, 1)], AtomSet.Empty);
+        const p02 = AtomSet.ofAtoms([p(0, 2)], AtomSet.Empty);
+        const p06 = AtomSet.ofAtoms([p(0, 6)], AtomSet.Empty);
+
+        const u0 = AtomSet.union([p01, p02, p06], template);
+        const u1 = AtomSet.union([p01, p02, p06, p13], template);
+        expect(AtomSet.unitGetById(u0, 0)).toBe(AtomSet.unitGetById(template, 0));
+        expect(AtomSet.unitGetById(u1, 0)).toBe(AtomSet.unitGetById(template, 0));
+        expect(AtomSet.unitGetById(u1, 1)).toBe(AtomSet.unitGetById(template, 1));
+        expect(u1).toBe(template);
+    });
+
     it('element at / index of', () => {
         const control: Atom[] = [];
-        const sets = Object.create(null);
+        const gen = AtomSet.Generator();
         for (let i = 1; i < 10; i++) {
             const set = [];
             for (let j = 1; j < 7; j++) {
                 control[control.length] = p(i * i, j * j + 1);
                 set[set.length] = j * j + 1;
             }
-            sets[i * i] = OrderedSet.ofSortedArray(set);
+            gen.add(i * i, AtomGroup.createNew(OrderedSet.ofSortedArray(set)));
         }
-        const ms = AtomSet.create(sets);
+        const ms = gen.getSet();
         for (let i = 0; i < control.length; i++) {
             expect(Atom.areEqual(AtomSet.atomGetAt(ms, i), control[i])).toBe(true);
         }
@@ -72,17 +117,17 @@ describe('atom set', () => {
     });
 
     it('packed pairs', () => {
-        const set = AtomSet.create([p(1, 3), p(0, 1), p(0, 6), p(0, 2)]);
+        const set = AtomSet.ofAtoms([p(1, 3), p(0, 1), p(0, 6), p(0, 2)], AtomSet.Empty);
         expect(setToPairs(set)).toEqual([p(0, 1), p(0, 2), p(0, 6), p(1, 3)]);
     });
 
     it('equality', () => {
-        const a = AtomSet.create([p(1, 3), p(0, 1), p(0, 6), p(0, 2)]);
-        const b = AtomSet.create([p(1, 3), p(0, 1), p(0, 6), p(0, 2)]);
-        const c = AtomSet.create([p(1, 3), p(0, 4), p(0, 6), p(0, 2)]);
-        const d = AtomSet.create([p(1, 3)]);
-        const e = AtomSet.create([p(1, 3)]);
-        const f = AtomSet.create([p(3, 3)]);
+        const a = AtomSet.ofAtoms([p(1, 3), p(0, 1), p(0, 6), p(0, 2)], AtomSet.Empty);
+        const b = AtomSet.ofAtoms([p(1, 3), p(0, 1), p(0, 6), p(0, 2)], AtomSet.Empty);
+        const c = AtomSet.ofAtoms([p(1, 3), p(0, 4), p(0, 6), p(0, 2)], AtomSet.Empty);
+        const d = AtomSet.ofAtoms([p(1, 3)], AtomSet.Empty);
+        const e = AtomSet.ofAtoms([p(1, 3)], AtomSet.Empty);
+        const f = AtomSet.ofAtoms([p(3, 3)], AtomSet.Empty);
 
         expect(AtomSet.areEqual(a, a)).toBe(true);
         expect(AtomSet.areEqual(a, b)).toBe(true);
@@ -94,13 +139,13 @@ describe('atom set', () => {
     });
 
     it('are intersecting', () => {
-        const a = AtomSet.create([p(1, 3), p(0, 1), p(0, 6), p(0, 2)]);
-        const b = AtomSet.create([p(1, 3), p(0, 1), p(0, 6), p(0, 2)]);
-        const c = AtomSet.create([p(1, 3), p(0, 4), p(0, 6), p(0, 2)]);
-        const d = AtomSet.create([p(1, 3)]);
-        const e = AtomSet.create([p(1, 3)]);
-        const f = AtomSet.create([p(3, 3)]);
-        const g = AtomSet.create([p(10, 3), p(8, 1), p(7, 6), p(3, 2)]);
+        const a = AtomSet.ofAtoms([p(1, 3), p(0, 1), p(0, 6), p(0, 2)], AtomSet.Empty);
+        const b = AtomSet.ofAtoms([p(1, 3), p(0, 1), p(0, 6), p(0, 2)], AtomSet.Empty);
+        const c = AtomSet.ofAtoms([p(1, 3), p(0, 4), p(0, 6), p(0, 2)], AtomSet.Empty);
+        const d = AtomSet.ofAtoms([p(1, 3)], AtomSet.Empty);
+        const e = AtomSet.ofAtoms([p(1, 3)], AtomSet.Empty);
+        const f = AtomSet.ofAtoms([p(3, 3)], AtomSet.Empty);
+        const g = AtomSet.ofAtoms([p(10, 3), p(8, 1), p(7, 6), p(3, 2)], AtomSet.Empty);
 
         expect(AtomSet.areIntersecting(a, a)).toBe(true);
         expect(AtomSet.areIntersecting(a, b)).toBe(true);
@@ -113,10 +158,10 @@ describe('atom set', () => {
     });
 
     it('intersection', () => {
-        const a = AtomSet.create([p(1, 3), p(0, 1), p(0, 6), p(0, 2)]);
-        const b = AtomSet.create([p(10, 3), p(0, 1), p(0, 6), p(4, 2)]);
-        const c = AtomSet.create([p(1, 3)]);
-        const d = AtomSet.create([p(2, 3)]);
+        const a = AtomSet.ofAtoms([p(1, 3), p(0, 1), p(0, 6), p(0, 2)], AtomSet.Empty);
+        const b = AtomSet.ofAtoms([p(10, 3), p(0, 1), p(0, 6), p(4, 2)], AtomSet.Empty);
+        const c = AtomSet.ofAtoms([p(1, 3)], AtomSet.Empty);
+        const d = AtomSet.ofAtoms([p(2, 3)], AtomSet.Empty);
         expect(AtomSet.intersect(a, a)).toBe(a);
         expect(setToPairs(AtomSet.intersect(a, b))).toEqual([p(0, 1), p(0, 6)]);
         expect(setToPairs(AtomSet.intersect(a, c))).toEqual([p(1, 3)]);
@@ -124,12 +169,12 @@ describe('atom set', () => {
     });
 
     it('subtract', () => {
-        const a = AtomSet.create([p(1, 3), p(0, 1), p(0, 6), p(0, 2)]);
-        const a1 = AtomSet.create([p(1, 3), p(0, 1), p(0, 6), p(0, 2)]);
-        const b = AtomSet.create([p(10, 3), p(0, 1), p(0, 6), p(4, 2)]);
-        const c = AtomSet.create([p(1, 3)]);
-        const d = AtomSet.create([p(2, 3)]);
-        const e = AtomSet.create([p(0, 2)]);
+        const a = AtomSet.ofAtoms([p(1, 3), p(0, 1), p(0, 6), p(0, 2)], AtomSet.Empty);
+        const a1 = AtomSet.ofAtoms([p(1, 3), p(0, 1), p(0, 6), p(0, 2)], AtomSet.Empty);
+        const b = AtomSet.ofAtoms([p(10, 3), p(0, 1), p(0, 6), p(4, 2)], AtomSet.Empty);
+        const c = AtomSet.ofAtoms([p(1, 3)], AtomSet.Empty);
+        const d = AtomSet.ofAtoms([p(2, 3)], AtomSet.Empty);
+        const e = AtomSet.ofAtoms([p(0, 2)], AtomSet.Empty);
         expect(setToPairs(AtomSet.subtract(a, a))).toEqual([]);
         expect(setToPairs(AtomSet.subtract(a, a1))).toEqual([]);
         expect(setToPairs(AtomSet.subtract(a, b))).toEqual([p(0, 2), p(1, 3)]);
@@ -141,17 +186,17 @@ describe('atom set', () => {
     });
 
     it('union', () => {
-        const a = AtomSet.create([p(1, 3), p(0, 1)]);
-        const a1 = AtomSet.create([p(1, 3), p(0, 1)]);
-        const b = AtomSet.create([p(10, 3), p(0, 1)]);
-        const c = AtomSet.create([p(1, 3)]);
-        const d = AtomSet.create([p(2, 3)]);
-        expect(AtomSet.unionMany([a])).toBe(a);
-        expect(AtomSet.union(a, a)).toBe(a);
-        expect(setToPairs(AtomSet.union(a, a))).toEqual([p(0, 1), p(1, 3)]);
-        expect(setToPairs(AtomSet.union(a, a1))).toEqual([p(0, 1), p(1, 3)]);
-        expect(setToPairs(AtomSet.union(a, b))).toEqual([p(0, 1), p(1, 3), p(10, 3)]);
-        expect(setToPairs(AtomSet.union(c, d))).toEqual([p(1, 3), p(2, 3)]);
-        expect(setToPairs(AtomSet.unionMany([a, b, c, d]))).toEqual([p(0, 1), p(1, 3), p(2, 3), p(10, 3)]);
+        const a = AtomSet.ofAtoms([p(1, 3), p(0, 1)], AtomSet.Empty);
+        const a1 = AtomSet.ofAtoms([p(1, 3), p(0, 1)], AtomSet.Empty);
+        const b = AtomSet.ofAtoms([p(10, 3), p(0, 1)], AtomSet.Empty);
+        const c = AtomSet.ofAtoms([p(1, 3)], AtomSet.Empty);
+        const d = AtomSet.ofAtoms([p(2, 3)], AtomSet.Empty);
+        expect(AtomSet.union([a], AtomSet.Empty)).toBe(a);
+        expect(AtomSet.union([a, a], AtomSet.Empty)).toBe(a);
+        expect(setToPairs(AtomSet.union([a, a], AtomSet.Empty))).toEqual([p(0, 1), p(1, 3)]);
+        expect(setToPairs(AtomSet.union([a, a1], AtomSet.Empty))).toEqual([p(0, 1), p(1, 3)]);
+        expect(setToPairs(AtomSet.union([a, b], AtomSet.Empty))).toEqual([p(0, 1), p(1, 3), p(10, 3)]);
+        expect(setToPairs(AtomSet.union([c, d], AtomSet.Empty))).toEqual([p(1, 3), p(2, 3)]);
+        expect(setToPairs(AtomSet.union([a, b, c, d], AtomSet.Empty))).toEqual([p(0, 1), p(1, 3), p(2, 3), p(10, 3)]);
     });
 });

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

@@ -7,5 +7,6 @@
 import Model from './model/model'
 import * as Types from './model/types'
 import Format from './model/format'
+import ModelSymmetry from './model/properties/symmetry'
 
-export { Model, Types, Format }
+export { Model, Types, Format, ModelSymmetry }

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

@@ -11,8 +11,10 @@ import Format from '../format'
 import Model from '../model'
 import * as Hierarchy from '../properties/hierarchy'
 import Conformation from '../properties/conformation'
+import Symmetry from '../properties/symmetry'
 import findHierarchyKeys from '../utils/hierarchy-keys'
 import { ElementSymbol} from '../types'
+import createAssemblies from './mmcif/assembly'
 
 import mmCIF_Format = Format.mmCIF
 
@@ -76,6 +78,10 @@ function getConformation({ data }: mmCIF_Format, bounds: Interval): Conformation
     }
 }
 
+function getSymmetry(format: mmCIF_Format): Symmetry {
+    return { assemblies: createAssemblies(format) };
+}
+
 function isHierarchyDataEqual(a: Hierarchy.Hierarchy, b: Hierarchy.Data) {
     // need to cast because of how TS handles type resolution for interfaces https://github.com/Microsoft/TypeScript/issues/15300
     return Table.areEqual(a.chains as Table<Hierarchy.ChainsSchema>, b.chains as Table<Hierarchy.ChainsSchema>)
@@ -105,6 +111,7 @@ function createModel(format: mmCIF_Format, bounds: Interval, previous?: Model):
         modelNum: format.data.atom_site.pdbx_PDB_model_num.value(Interval.start(bounds)),
         hierarchy: { ...hierarchyData, ...hierarchyKeys, ...hierarchySegments },
         conformation: getConformation(format, bounds),
+        symmetry: getSymmetry(format),
         atomCount: Interval.size(bounds)
     };
 }

+ 149 - 0
src/mol-model/structure/model/formats/mmcif/assembly.ts

@@ -0,0 +1,149 @@
+/**
+ * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { Mat4, Tensor } from 'mol-math/linear-algebra'
+import SymmetryOperator from 'mol-math/geometry/symmetry-operator'
+import Format from '../../format'
+import { Assembly, OperatorGroup, OperatorGroups } from '../../properties/symmetry'
+import { Queries as Q } from '../../../query'
+
+import mmCIF_Format = Format.mmCIF
+
+export default function create(format: mmCIF_Format): ReadonlyArray<Assembly> {
+    const { pdbx_struct_assembly } = format.data;
+    if (!pdbx_struct_assembly._rowCount) return [];
+
+    const matrices = getMatrices(format);
+    const assemblies: Assembly[] = [];
+    for (let i = 0; i < pdbx_struct_assembly._rowCount; i++) {
+        assemblies[assemblies.length] = createAssembly(format, i, matrices);
+    }
+    return assemblies;
+}
+
+type Matrices = Map<string, Mat4>
+type Generator = { expression: string, asymIds: string[] }
+
+function createAssembly(format: mmCIF_Format, index: number, matrices: Matrices): Assembly {
+    const { pdbx_struct_assembly, pdbx_struct_assembly_gen } = format.data;
+
+    const id = pdbx_struct_assembly.id.value(index);
+    const details = pdbx_struct_assembly.details.value(index);
+    const generators: Generator[] = [];
+
+    const { assembly_id, oper_expression, asym_id_list } = pdbx_struct_assembly_gen;
+
+    for (let i = 0, _i = pdbx_struct_assembly_gen._rowCount; i < _i; i++) {
+        if (assembly_id.value(i) !== id) continue;
+        generators[generators.length] = {
+            expression: oper_expression.value(i),
+            asymIds: asym_id_list.value(i).split(',').map(x => x.trim()).filter(x => !!x)
+        };
+    }
+
+    return Assembly.create(id, details, operatorGroupsProvider(generators, matrices));
+}
+
+function operatorGroupsProvider(generators: Generator[], matrices: Matrices): () => OperatorGroups {
+    return () => {
+        const groups: OperatorGroup[] = [];
+
+        let operatorOffset = 0;
+        for (let i = 0; i < generators.length; i++) {
+            const gen = generators[i];
+            const operatorList = parseOperatorList(gen.expression);
+            const operatorNames = expandOperators(operatorList);
+            const operators = getAssemblyOperators(matrices, operatorNames, operatorOffset);
+            const selector = 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)
+            )});
+            groups[groups.length] = { selector, operators };
+            operatorOffset += operators.length;
+        }
+
+        return groups;
+    }
+}
+
+function getMatrices({ data }: mmCIF_Format): Matrices {
+    const { pdbx_struct_oper_list } = data;
+    const { id, matrix, vector, _schema } = pdbx_struct_oper_list;
+    const matrices = new Map<string, Mat4>();
+
+    for (let i = 0, _i = pdbx_struct_oper_list._rowCount; i < _i; i++) {
+        const m = Tensor.toMat4(_schema.matrix.space, matrix.value(i));
+        const t = Tensor.toVec3(_schema.vector.space, vector.value(i));
+        Mat4.setTranslation(m, t);
+        Mat4.setValue(m, 3, 3, 1);
+        matrices.set(id.value(i), m);
+    }
+
+    return matrices;
+}
+
+function expandOperators(operatorList: string[][]) {
+    const ops: string[][] = [];
+    const currentOp: string[] = [];
+    for (let i = 0; i < operatorList.length; i++) currentOp[i] = '';
+    expandOperators1(operatorList, ops, operatorList.length - 1, currentOp);
+    return ops;
+}
+
+function expandOperators1(operatorNames: string[][], list: string[][], i: number, current: string[]) {
+    if (i < 0) {
+        list[list.length] = current.slice(0);
+        return;
+    }
+
+    let ops = operatorNames[i], len = ops.length;
+    for (let j = 0; j < len; j++) {
+        current[i] = ops[j];
+        expandOperators1(operatorNames, list, i - 1, current);
+    }
+}
+
+function getAssemblyOperators(matrices: Matrices, operatorNames: string[][], startIndex: number) {
+    const operators: SymmetryOperator[] = [];
+
+    let index = startIndex;
+    for (let op of operatorNames) {
+        let m = Mat4.identity();
+        for (let i = 0; i < op.length; i++) {
+            Mat4.mul(m, m, matrices.get(op[i])!);
+        }
+        index++;
+        operators[operators.length] = SymmetryOperator.create(`A-${index}`, m);
+    }
+
+    return operators;
+}
+
+function parseOperatorList(value: string): string[][] {
+    // '(X0)(1-5)' becomes [['X0'], ['1', '2', '3', '4', '5']]
+    // kudos to Glen van Ginkel.
+
+    const oeRegex = /\(?([^\(\)]+)\)?]*/g, groups: string[] = [], ret: string[][] = [];
+
+    let g: any;
+    while (g = oeRegex.exec(value)) groups[groups.length] = g[1];
+
+    groups.forEach(g => {
+        const group: string[] = [];
+        g.split(',').forEach(e => {
+            const dashIndex = e.indexOf('-');
+            if (dashIndex > 0) {
+                const from = parseInt(e.substring(0, dashIndex)), to = parseInt(e.substr(dashIndex + 1));
+                for (let i = from; i <= to; i++) group[group.length] = i.toString();
+            } else {
+                group[group.length] = e.trim();
+            }
+        });
+        ret[ret.length] = group;
+    });
+
+    return ret;
+}

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

@@ -6,8 +6,9 @@
 
 import UUID from 'mol-util/uuid'
 import Format from './format'
-import HierarchyProperties from './properties/hierarchy'
-import ConformationProperties from './properties/conformation'
+import Hierarchy from './properties/hierarchy'
+import Conformation from './properties/conformation'
+import Symmetry from './properties/symmetry'
 import from_mmCIF from './formats/mmcif'
 
 
@@ -23,8 +24,9 @@ interface Model extends Readonly<{
 
     sourceData: Format,
 
-    hierarchy: HierarchyProperties,
-    conformation: ConformationProperties,
+    hierarchy: Hierarchy,
+    conformation: Conformation,
+    symmetry: Symmetry,
 
     atomCount: number
 }> { }

+ 56 - 0
src/mol-model/structure/model/properties/symmetry.ts

@@ -0,0 +1,56 @@
+/**
+ * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import SymmetryOperator from 'mol-math/geometry/symmetry-operator'
+import { arrayFind } from 'mol-data/util'
+import { Query } from '../../query'
+import { Model } from '../../model'
+
+/** Determine an atom set and a list of operators that should be applied to that set  */
+export interface OperatorGroup {
+    readonly selector: Query,
+    readonly operators: ReadonlyArray<SymmetryOperator>
+}
+
+export type OperatorGroups = ReadonlyArray<OperatorGroup>
+
+export class Assembly {
+    readonly id: string;
+    readonly details: string;
+
+    private _operators: OperatorGroups;
+    get operatorGroups(): OperatorGroups {
+        if (this._operators) return this._operators;
+        this._operators = this.operatorsProvider();
+        return this._operators;
+    }
+
+    constructor(id: string, details: string, private operatorsProvider: () => OperatorGroups) {
+        this.id = id;
+        this.details = details;
+    }
+}
+
+export namespace Assembly {
+    export function create(id: string, details: string, operatorsProvider: () => OperatorGroups): Assembly {
+        return new Assembly(id, details, operatorsProvider);
+    }
+}
+
+interface Symmetry {
+    readonly assemblies: ReadonlyArray<Assembly>,
+}
+
+namespace Symmetry {
+    export const Empty: Symmetry = { assemblies: [] };
+
+    export function findAssembly(model: Model, id: string): Assembly | undefined {
+        const _id = id.toLocaleLowerCase();
+        return arrayFind(model.symmetry.assemblies, a => a.id.toLowerCase() === _id);
+    }
+}
+
+export default Symmetry

+ 0 - 9
src/mol-model/structure/model/properties/transforms.ts

@@ -1,9 +0,0 @@
-/**
- * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- */
-
-// TODO: symmetry and assebmlies descriptors
-
-//import Column from 'mol-base/collections/column'

+ 28 - 22
src/mol-model/structure/query/generators.ts

@@ -10,9 +10,9 @@ import P from './properties'
 import { Structure, AtomSet, Atom } from '../structure'
 import { OrderedSet, Segmentation } from 'mol-data/int'
 
-export const all: Query = s => s;
+export const all: Query = s => Selection.Singletons(s, s.atoms);
 
-export interface AtomGroupsParams {
+export interface AtomQueryParams {
     entityTest: Atom.Predicate,
     chainTest: Atom.Predicate,
     residueTest: Atom.Predicate,
@@ -20,11 +20,18 @@ export interface AtomGroupsParams {
     groupBy: Atom.Property<any>
 }
 
-export function atoms(params?: Partial<AtomGroupsParams>): Query {
+export interface AtomGroupsQueryParams extends AtomQueryParams {
+    groupBy: Atom.Property<any>
+}
+
+export function residues(params?: Partial<AtomQueryParams>) { return atoms({ ...params, groupBy: P.residue.key }); }
+export function chains(params?: Partial<AtomQueryParams>) { return atoms({ ...params, groupBy: P.chain.key }); }
+
+export function atoms(params?: Partial<AtomGroupsQueryParams>): Query {
     if (!params || (!params.atomTest && !params.residueTest && !params.chainTest && !params.entityTest && !params.groupBy)) return all;
     if (!!params.atomTest && !params.residueTest && !params.chainTest && !params.entityTest && !params.groupBy) return atomGroupsLinear(params.atomTest);
 
-    const normalized: AtomGroupsParams = {
+    const normalized: AtomGroupsQueryParams = {
         entityTest: params.entityTest || P.constant.true,
         chainTest: params.chainTest || P.constant.true,
         residueTest: params.residueTest || P.constant.true,
@@ -46,7 +53,7 @@ function atomGroupsLinear(atomTest: Atom.Predicate): Query {
         for (let i = 0, _i = unitIds.length; i < _i; i++) {
             const unitId = unitIds[i];
             l.unit = units[unitId];
-            const set = AtomSet.unitGetByIndex(atoms, i);
+            const set = AtomSet.unitGetByIndex(atoms, i).atoms;
 
             builder.beginUnit();
             for (let j = 0, _j = OrderedSet.size(set); j < _j; j++) {
@@ -56,11 +63,11 @@ function atomGroupsLinear(atomTest: Atom.Predicate): Query {
             builder.commitUnit(unitId);
         }
 
-        return Structure.create(units, builder.getSet());
+        return Selection.Singletons(structure, builder.getSet());
     };
 }
 
-function atomGroupsSegmented({ entityTest, chainTest, residueTest, atomTest }: AtomGroupsParams): Query {
+function atomGroupsSegmented({ entityTest, chainTest, residueTest, atomTest }: AtomGroupsQueryParams): Query {
     return structure => {
         const { atoms, units } = structure;
         const unitIds = AtomSet.unitIds(atoms);
@@ -71,7 +78,7 @@ function atomGroupsSegmented({ entityTest, chainTest, residueTest, atomTest }: A
             const unitId = unitIds[i];
             const unit = units[unitId];
             l.unit = unit;
-            const set = AtomSet.unitGetByIndex(atoms, i);
+            const set = AtomSet.unitGetByIndex(atoms, i).atoms;
 
             builder.beginUnit();
             const chainsIt = Segmentation.transientSegments(unit.hierarchy.chainSegments, set);
@@ -99,20 +106,20 @@ function atomGroupsSegmented({ entityTest, chainTest, residueTest, atomTest }: A
             builder.commitUnit(unitId);
         }
 
-        return Structure.create(units, builder.getSet());
+        return Selection.Singletons(structure, builder.getSet());
     };
 }
 
 class LinearGroupingBuilder {
     private builders: AtomSet.Builder[] = [];
-    private builderMap: { [key: string]: AtomSet.Builder } = Object.create(null);
+    private builderMap = new Map<string, AtomSet.Builder>();
 
     add(key: any, unit: number, atom: number) {
-        let b = this.builderMap[key];
+        let b = this.builderMap.get(key);
         if (!b) {
             b = AtomSet.LinearBuilder(this.structure.atoms);
             this.builders[this.builders.length] = b;
-            this.builderMap[key] = b;
+            this.builderMap.set(key, b);
         }
         b.add(unit, atom);
     }
@@ -124,34 +131,33 @@ class LinearGroupingBuilder {
         return true;
     }
 
-    private singletonStructure(): Structure {
+    private singletonSelection(): Selection {
         const atoms: Atom[] = Atom.createEmptyArray(this.builders.length);
         for (let i = 0, _i = this.builders.length; i < _i; i++) {
             atoms[i] = this.builders[i].singleton();
         }
-        return Structure.create(this.structure.units, AtomSet.create(atoms));
+        return Selection.Singletons(this.structure, AtomSet.ofAtoms(atoms, this.structure.atoms));
     }
 
     private fullSelection() {
-        const ret: Structure[] = [];
+        const sets: AtomSet[] = new Array(this.builders.length);
         for (let i = 0, _i = this.builders.length; i < _i; i++) {
-            ret[i] = Structure.create(this.structure.units, this.builders[i].getSet());
+            sets[i] = this.builders[i].getSet();
         }
-        return ret;
+        return Selection.Sequence(this.structure, sets);
     }
 
     getSelection(): Selection {
         const len = this.builders.length;
-        if (len === 0) return Selection.Empty;
-        if (len === 1) return Structure.create(this.structure.units, this.builders[0].getSet());
-        if (this.allSingletons()) return this.singletonStructure();
+        if (len === 0) return Selection.Empty(this.structure);
+        if (this.allSingletons()) return this.singletonSelection();
         return this.fullSelection();
     }
 
     constructor(private structure: Structure) { }
 }
 
-function atomGroupsGrouped({ entityTest, chainTest, residueTest, atomTest, groupBy }: AtomGroupsParams): Query {
+function atomGroupsGrouped({ entityTest, chainTest, residueTest, atomTest, groupBy }: AtomGroupsQueryParams): Query {
     return structure => {
         const { atoms, units } = structure;
         const unitIds = AtomSet.unitIds(atoms);
@@ -162,7 +168,7 @@ function atomGroupsGrouped({ entityTest, chainTest, residueTest, atomTest, group
             const unitId = unitIds[i];
             const unit = units[unitId];
             l.unit = unit;
-            const set = AtomSet.unitGetByIndex(atoms, i);
+            const set = AtomSet.unitGetByIndex(atoms, i).atoms;
 
             const chainsIt = Segmentation.transientSegments(unit.hierarchy.chainSegments, set);
             const residuesIt = Segmentation.transientSegments(unit.hierarchy.residueSegments, set);

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

@@ -64,7 +64,7 @@ namespace Predicates {
 
     export function or(...ps: Atom.Predicate[]): Atom.Predicate {
         switch (ps.length) {
-            case 0: return P.constant.true;
+            case 0: return P.constant.false;
             case 1: return ps[0];
             case 2: {
                 const a = ps[0], b = ps[1];

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

@@ -7,5 +7,7 @@
 import { Structure } from '../structure'
 import Selection from './selection'
 
+// TODO: Query { (s: Structure): Computation<Selection> }
+
 interface Query { (s: Structure): Selection }
 export default Query

+ 65 - 75
src/mol-model/structure/query/selection.ts

@@ -4,121 +4,111 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import Iterator from 'mol-data/iterator'
-import HashSet from 'mol-data/util/hash-set'
-import { Structure, Atom, AtomSet } from '../structure'
+import { HashSet } from 'mol-data/util'
+import { Structure, AtomSet } from '../structure'
 
-type Selection =
-    | Structure // each atom is interpreted as a singleton structure
-    | Structure[]
+// A selection is a pair of a Structure and a sequence of unique AtomSets
+type Selection = Selection.Singletons | Selection.Sequence
 
 namespace Selection {
-    export const Empty: Selection = [];
+    // If each element of the selection is a singleton, we can use a more efficient representation.
+    export interface Singletons { readonly kind: 'singletons', readonly structure: Structure, readonly set: AtomSet }
+    export interface Sequence { readonly kind: 'sequence', readonly structure: Structure, readonly sets: ReadonlyArray<AtomSet> }
 
-    function isStructure(x: Selection): x is Structure { return !!(x as Structure).units && !!(x as Structure).atoms; }
+    export function Singletons(structure: Structure, set: AtomSet): Singletons { return { kind: 'singletons', structure, set } }
+    export function Sequence(structure: Structure, sets: AtomSet[]): Sequence { return { kind: 'sequence', structure, sets } }
+    export function Empty(structure: Structure): Selection { return Sequence(structure, []); };
+
+    export function isSingleton(s: Selection): s is Singletons { return s.kind === 'singletons'; }
+    export function isEmpty(s: Selection) { return isSingleton(s) ? AtomSet.atomCount(s.set) === 0 : s.sets.length === 0; }
 
     export function structureCount(sel: Selection) {
-        if (isStructure(sel)) return AtomSet.atomCount(sel.atoms);
-        return sel.length;
+        if (isSingleton(sel)) return AtomSet.atomCount(sel.set);
+        return sel.sets.length;
     }
 
-    export function union(sel: Selection): Structure {
-        if (isStructure(sel)) return sel;
-        if (!sel.length) return Structure.Empty;
-        const sets = [];
-        for (let i = 0, _i = sel.length; i < _i; i++) sets[sets.length] = sel[i].atoms;
-        return Structure.create(unionUnits(sel), AtomSet.unionMany(sets));
+    export function unionStructure(sel: Selection): Structure {
+        if (isEmpty(sel)) return Structure.Empty(sel.structure.units);
+        if (isSingleton(sel)) return Structure.create(sel.structure.units, sel.set);
+        return Structure.create(sel.structure.units, AtomSet.union(sel.sets, sel.structure.atoms));
     }
 
-    export function structures(sel: Selection): Iterator<Structure> {
-        if (isStructure(sel)) {
-            const units = sel.units;
-            return Iterator.map<Atom, Structure>(AtomSet.atoms(sel.atoms), atoms => Structure.create(units, atoms));
+    export function getAt(sel: Selection, i: number): Structure {
+        if (isSingleton(sel)) {
+            const atom = AtomSet.atomGetAt(sel.set, i);
+            return Structure.create(sel.structure.units, AtomSet.singleton(atom, sel.structure.atoms));
         }
-        return Iterator.Array(sel);
+        return Structure.create(sel.structure.units, sel.sets[i]);
     }
 
-    export function getAt(sel: Selection, i: number): Structure {
-        if (isStructure(sel)) {
-            return Structure.create(sel.units, AtomSet.atomGetAt(sel.atoms, i));
+    export function toStructures(sel: Selection): Structure[] {
+        const { units } = sel.structure;
+        if (isSingleton(sel)) {
+            const ret: Structure[] = new Array(AtomSet.atomCount(sel.set));
+            const atoms = AtomSet.atoms(sel.set);
+            let offset = 0;
+            while (atoms.hasNext) {
+                const atom = atoms.move();
+                ret[offset++] = Structure.create(units, AtomSet.singleton(atom, sel.structure.atoms))
+            }
+            return ret;
+        } else {
+            const { sets } = sel;
+            const ret: Structure[] = new Array(sets.length);
+            for (let i = 0, _i = sets.length; i < _i; i++) ret[i] = Structure.create(units, sets[i]);
+            return ret;
         }
-        return sel[i];
     }
 
     export interface Builder {
-        add(s: Structure): void,
+        add(set: AtomSet): void,
         getSelection(): Selection
     }
 
+    function getSelection(structure: Structure, sets: AtomSet[], allSingletons: boolean) {
+        const len = sets.length;
+        if (len === 0) return Empty(structure);
+        if (allSingletons) return Singletons(structure, AtomSet.union(sets, structure.atoms));
+        return Sequence(structure, sets);
+    }
+
     class LinearBuilderImpl implements Builder {
-        private structures: Structure[] = [];
+        private sets: AtomSet[] = [];
         private allSingletons = true;
 
-        add(s: Structure) {
-            const atomCount = AtomSet.atomCount(s.atoms);
+        add(atoms: AtomSet) {
+            const atomCount = AtomSet.atomCount(atoms);
             if (atomCount === 0) return;
-            this.structures[this.structures.length] = s;
+            this.sets[this.sets.length] = atoms;
             if (atomCount !== 1) this.allSingletons = false;
         }
 
-        getSelection() {
-            const len = this.structures.length;
-            if (len === 0) return Empty;
-            if (len === 1) return this.structures[0];
-            if (this.allSingletons) return union(this.structures);
-            return this.structures;
-        }
+        getSelection() { return getSelection(this.structure, this.sets, this.allSingletons); }
 
-        constructor() { }
+        constructor(private structure: Structure) { }
     }
 
     class HashBuilderImpl implements Builder {
-        private structures: Structure[] = [];
+        private sets: AtomSet[] = [];
         private allSingletons = true;
-        private sets = HashSet(AtomSet.hashCode, AtomSet.areEqual);
+        private uniqueSets = HashSet(AtomSet.hashCode, AtomSet.areEqual);
 
-        add(s: Structure) {
-            const atomCount = AtomSet.atomCount(s.atoms);
-            if (atomCount === 0 || !this.sets.add(s.atoms)) return;
-            this.structures[this.structures.length] = s;
+        add(atoms: AtomSet) {
+            const atomCount = AtomSet.atomCount(atoms);
+            if (atomCount === 0 || !this.uniqueSets.add(atoms)) return;
+            this.sets[this.sets.length] = atoms;
             if (atomCount !== 1) this.allSingletons = false;
         }
 
-        getSelection() {
-            const len = this.structures.length;
-            if (len === 0) return Empty;
-            if (len === 1) return this.structures[0];
-            if (this.allSingletons) return union(this.structures);
-            return this.structures;
-        }
+        getSelection() { return getSelection(this.structure, this.sets, this.allSingletons); }
 
-        constructor() { }
+        constructor(private structure: Structure) { }
     }
 
-    export function LinearBuilder(): Builder { return new LinearBuilderImpl(); }
-    export function UniqueBuilder(): Builder { return new HashBuilderImpl(); }
+    export function LinearBuilder(structure: Structure): Builder { return new LinearBuilderImpl(structure); }
+    export function UniqueBuilder(structure: Structure): Builder { return new HashBuilderImpl(structure); }
 
     // TODO: spatial lookup
 }
 
-export default Selection
-
-function unionUnits(xs: Structure[]): Structure['units'] {
-    let prev = xs[0].units;
-    let sameModel = true;
-    for (let i = 1, _i = xs.length; i < _i; i++) {
-        if (xs[i].units !== prev) sameModel = false;
-    }
-    if (sameModel) return prev;
-
-    let ret: any = { ...prev };
-    for (let i = 1, _i = xs.length; i < _i; i++) {
-        const units = xs[i].units;
-        if (units !== prev) {
-            const keys = Object.keys(units);
-            for (let j = 0; j < keys.length; j++) ret[keys[j]] = (units as any)[keys[j]];
-        }
-        prev = xs[i];
-    }
-    return ret;
-}
+export default Selection

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

@@ -6,7 +6,9 @@
 
 import Atom from './structure/atom'
 import AtomSet from './structure/atom/set'
+import AtomGroup from './structure/atom/group'
 import Structure from './structure/structure'
 import Unit from './structure/unit'
+import Symmetry from './structure/symmetry'
 
-export { Atom, AtomSet, Structure, Unit }
+export { Atom, AtomSet, AtomGroup, Structure, Unit, Symmetry }

+ 71 - 0
src/mol-model/structure/structure/atom/group.ts

@@ -0,0 +1,71 @@
+/**
+ * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { OrderedSet } from 'mol-data/int'
+import Unit from '../unit'
+
+interface AtomGroup {
+    atoms: OrderedSet,
+    id: number
+}
+
+namespace AtomGroup {
+    export const Empty = createNew(OrderedSet.Empty)
+
+    export function singleton(idx: number) {
+        return createNew(OrderedSet.ofSingleton(idx));
+    }
+
+    export function createNew(atoms: OrderedSet): AtomGroup {
+        return { id: nextId(), atoms };
+    }
+
+    export function create(unit: Unit, atoms: OrderedSet): AtomGroup {
+        if (OrderedSet.areEqual(atoms, unit.fullGroup.atoms)) return unit.fullGroup;
+        return createNew(atoms);
+    }
+
+    export function createChild(parent: AtomGroup, atoms: OrderedSet): AtomGroup {
+        if (OrderedSet.areEqual(atoms, parent.atoms)) return parent;
+        return createNew(atoms);
+    }
+
+    export function size(group: AtomGroup) { return OrderedSet.size(group.atoms); }
+    export function has(group: AtomGroup, atom: number) { return OrderedSet.has(group.atoms, atom); }
+    export function getAt(group: AtomGroup, i: number) { return OrderedSet.getAt(group.atoms, i); }
+    export function indexOf(group: AtomGroup, atom: number) { return OrderedSet.indexOf(group.atoms, atom); }
+    export function hashCode(group: AtomGroup) { return OrderedSet.hashCode(group.atoms); }
+    export function areEqual(a: AtomGroup, b: AtomGroup) { return OrderedSet.areEqual(a.atoms, b.atoms); }
+
+    export function intersect(a: AtomGroup, b: AtomGroup) {
+        const set = OrderedSet.intersect(a.atoms, b.atoms);
+        if (set === a.atoms) return a;
+        if (set === b.atoms) return b;
+        return createNew(set);
+    }
+
+    export function union(a: AtomGroup, b: AtomGroup) {
+        const set = OrderedSet.union(a.atoms, b.atoms);
+        if (set === a.atoms) return a;
+        if (set === b.atoms) return b;
+        return createNew(set);
+    }
+
+    export function subtract(a: AtomGroup, b: AtomGroup) {
+        const set = OrderedSet.subtract(a.atoms, b.atoms);
+        if (set === a.atoms) return a;
+        return createNew(set);
+    }
+
+    let _id = 0;
+    function nextId() {
+        const ret = _id;
+        _id = (_id + 1) % 0x3fffffff;
+        return ret;
+    }
+}
+
+export default AtomGroup

+ 0 - 0
src/mol-model/structure/structure/atom/set/properties.ts → src/mol-model/structure/structure/atom/impl/properties.ts


+ 17 - 22
src/mol-model/structure/structure/atom/set/builder.ts → src/mol-model/structure/structure/atom/impl/set-builder.ts

@@ -6,21 +6,21 @@
 
 import AtomSet from '../set'
 import Atom from '../../atom'
-import { OrderedSet } from 'mol-data/int'
+import { OrderedSet, IntMap } from 'mol-data/int'
 import { sortArray } from 'mol-data/util/sort'
 
 export class Builder {
     private keys: number[] = [];
-    private units: number[][] = Object.create(null);
+    private units = IntMap.Mutable<number[]>();
     private currentUnit: number[] = [];
 
     atomCount = 0;
 
     add(u: number, a: number) {
-        const unit = this.units[u];
+        const unit = this.units.get(u);
         if (!!unit) { unit[unit.length] = a; }
         else {
-            this.units[u] = [a];
+            this.units.set(u, [a]);
             this.keys[this.keys.length] = u;
         }
         this.atomCount++;
@@ -31,40 +31,35 @@ export class Builder {
     commitUnit(u: number) {
         if (this.currentUnit.length === 0) return;
         this.keys[this.keys.length] = u;
-        this.units[u] = this.currentUnit;
+        this.units.set(u, this.currentUnit);
     }
 
     getSet(): AtomSet {
-        const sets: { [key: number]: OrderedSet } = Object.create(null);
-
-        let allEqual = this.keys.length === AtomSet.unitCount(this.parent);
+        const generator = AtomSet.TemplateGenerator(this.parent);
 
         for (let i = 0, _i = this.keys.length; i < _i; i++) {
             const k = this.keys[i];
-            const unit = this.units[k];
+            const unit = this.units.get(k);
             const l = unit.length;
             if (!this.sorted && l > 1) sortArray(unit);
-
-            const set = l === 1 ? OrderedSet.ofSingleton(unit[0]) : OrderedSet.ofSortedArray(unit);
-            const parentSet = AtomSet.unitGetById(this.parent, k);
-            if (OrderedSet.areEqual(set, parentSet)) {
-                sets[k] = parentSet;
-            } else {
-                sets[k] = set;
-                allEqual = false;
-            }
+            generator.add(k, OrderedSet.ofSortedArray(unit));
         }
-        return allEqual ? this.parent : AtomSet.create(sets);
+
+        return generator.getSet();
     }
 
     singleton(): Atom {
         const u = this.keys[0];
-        return Atom.create(u, this.units[u][0]);
+        return Atom.create(u, this.units.get(u)[0]);
     }
 
     constructor(private parent: AtomSet, private sorted: boolean) { }
 }
 
-export default function createBuilder(parent: AtomSet, sorted: boolean) {
-    return new Builder(parent, sorted);
+export function LinearBuilder(parent: AtomSet) {
+    return new Builder(parent, true);
+}
+
+export function UnsortedBuilder(parent: AtomSet) {
+    return new Builder(parent, false);
 }

+ 448 - 0
src/mol-model/structure/structure/atom/impl/set.ts

@@ -0,0 +1,448 @@
+/**
+ * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { SortedArray, Interval, Iterator, OrderedSet as OS, IntMap } from 'mol-data/int'
+import { sortArray } from 'mol-data/util/sort'
+import { hash1 } from 'mol-data/util/hash-functions'
+import Atom from '../../atom'
+import AtomGroup from '../group'
+
+/** Long and painful implementation starts here */
+
+export type AtomSetImpl = { groups: IntMap<AtomGroup>, offsets: Int32Array, hashCode: number, keys: SortedArray }
+
+export const Empty: AtomSetImpl = { groups: IntMap.Empty, offsets: new Int32Array(1), hashCode: 0, keys: SortedArray.Empty };
+
+export function ofAtoms(atoms: ArrayLike<Atom>, template: AtomSetImpl): AtomSetImpl {
+    return ofAtomsImpl(atoms, template);
+}
+
+export function singleton(atom: Atom, template: AtomSetImpl) {
+    return singletonImpl(atom, template);
+}
+
+export function getKeys(set: AtomSetImpl): SortedArray {
+    return set.keys;
+}
+
+export function keyCount(set: AtomSetImpl): number {
+    return set.keys.length;
+}
+
+export function hasKey(set: AtomSetImpl, key: number): boolean {
+    return set.groups.has(key);
+}
+
+export function getKey(set: AtomSetImpl, index: number): number {
+    return set.keys[index];
+}
+
+export function hasAtom(set: AtomSetImpl, t: Atom): boolean {
+    const os = set.groups.get(Atom.unit(t));
+    return !!os && AtomGroup.has(os, Atom.index(t));
+}
+
+export function getByKey(set: AtomSetImpl, key: number): AtomGroup {
+    return set.groups.get(key) || AtomGroup.Empty;
+}
+
+export function getByIndex(set: AtomSetImpl, index: number): AtomGroup {
+    const key = set.keys[index];
+    return set.groups.get(key) || AtomGroup.Empty;
+}
+
+export function getAt(set: AtomSetImpl, i: number): Atom {
+    const { offsets, keys } = set;
+    const o = getOffsetIndex(offsets, i);
+    if (o >= offsets.length - 1) return Atom.Zero;
+    const k = keys[o];
+    const e = AtomGroup.getAt(set.groups.get(k), i - offsets[o]);
+    return Atom.create(k, e);
+}
+
+export function indexOf(set: AtomSetImpl, t: Atom) {
+    const { keys } = set;
+    const u = Atom.unit(t);
+    const setIdx = SortedArray.indexOf(keys, u);
+    if (setIdx < 0) return -1;
+    const o = AtomGroup.indexOf(set.groups.get(u), Atom.index(t));
+    if (o < 0) return -1;
+    return set.offsets[setIdx] + o;
+}
+
+/** Number elements in the "child" sets */
+export function size(set: AtomSetImpl) {
+    return set.offsets[set.offsets.length - 1];
+}
+
+export function hashCode(set: AtomSetImpl) {
+    if (set.hashCode !== -1) return set.hashCode;
+    return computeHash(set);
+}
+
+export function areEqual(a: AtomSetImpl, b: AtomSetImpl) {
+    if (a === b) return true;
+    if (size(a) !== size(a)) return false;
+
+    const keys = a.keys;
+    if (!SortedArray.areEqual(keys, b.keys)) return false;
+    const { groups: aG } = a;
+    const { groups: bG } = b;
+    for (let i = 0, _i = keys.length; i < _i; i++) {
+        const k = keys[i];
+        if (!AtomGroup.areEqual(aG.get(k), bG.get(k))) return false;
+    }
+    return true;
+}
+
+export function areIntersecting(a: AtomSetImpl, b: AtomSetImpl) {
+    if (a === b) return true;
+    const keysA = a.keys, keysB = b.keys;
+    if (!SortedArray.areIntersecting(a.keys, b.keys)) return false;
+    const r = SortedArray.findRange(keysA, SortedArray.min(keysB), SortedArray.max(keysB));
+    const start = Interval.start(r), end = Interval.end(r);
+    const { groups: aG } = a;
+    const { groups: bG } = b;
+    for (let i = start; i < end; i++) {
+        const k = keysA[i];
+        const ak = aG.get(k), bk = bG.get(k);
+        if (!!ak && !!bk && OS.areIntersecting(ak.atoms, bk.atoms)) return true;
+    }
+    return false;
+}
+
+export function intersect(a: AtomSetImpl, b: AtomSetImpl) {
+    if (a === b) return a;
+
+    const keysA = a.keys, keysB = b.keys;
+    if (!SortedArray.areIntersecting(a.keys, b.keys)) return Empty;
+    const r = SortedArray.findRange(keysA, SortedArray.min(keysB), SortedArray.max(keysB));
+    const start = Interval.start(r), end = Interval.end(r);
+
+    const { groups: aG } = a;
+    const { groups: bG } = b;
+    const generator = new ChildGenerator(a, b);
+    for (let i = start; i < end; i++) {
+        const k = keysA[i];
+        const bk = bG.get(k);
+        if (!bk) continue;
+        const ak = aG.get(k);
+        generator.add(k, AtomGroup.intersect(aG.get(k), bk), ak, bk);
+    }
+    return generator.getSet();
+}
+
+export function subtract(a: AtomSetImpl, b: AtomSetImpl) {
+    if (a === b) return Empty;
+
+    const keysA = a.keys, keysB = b.keys;
+    if (!SortedArray.areIntersecting(keysA, keysB)) return a;
+    const r = SortedArray.findRange(keysA, SortedArray.min(keysB), SortedArray.max(keysB));
+    const start = Interval.start(r), end = Interval.end(r);
+
+    const generator = new ChildGenerator(a, b);
+    const { groups: aG } = a;
+    const { groups: bG } = b;
+    for (let i = 0; i < start; i++) {
+        const k = keysA[i];
+        const ak = aG.get(k);
+        generator.addA(k, ak, ak);
+    }
+    for (let i = start; i < end; i++) {
+        const k = keysA[i];
+        const ak = aG.get(k), bk = bG.get(k);
+        if (!!bk) {
+            const subtraction = AtomGroup.subtract(ak, bk);
+            generator.addA(k, subtraction, ak);
+        } else {
+            generator.addA(k, ak, ak);
+        }
+    }
+    for (let i = end, _i = keysA.length; i < _i; i++) {
+        const k = keysA[i];
+        const ak = aG.get(k);
+        generator.addA(k, ak, ak);
+    }
+    return generator.getSet();
+}
+
+export function unionMany(sets: ArrayLike<AtomSetImpl>, template: AtomSetImpl) {
+    return findUnion(sets, template);
+}
+
+class ElementsIterator implements Iterator<Atom> {
+    private unit: number = 0;
+    private keyCount: number;
+    private setIndex = -1;
+    private currentIndex = 0;
+    private currentSize = 0;
+    private currentSet: OS = OS.Empty;
+
+    hasNext: boolean = false;
+
+    move() {
+        if (!this.hasNext) return Atom.Zero;
+        const ret = Atom.create(this.unit, OS.getAt(this.currentSet, this.currentIndex++));
+        if (this.currentIndex >= this.currentSize) this.advance();
+        return ret;
+    }
+
+    private advance() {
+        if (++this.setIndex >= this.keyCount) {
+            this.hasNext = false;
+            return false;
+        }
+        this.unit = this.elements.keys[this.setIndex];
+        this.currentSet = this.elements.groups.get(this.unit).atoms;
+        this.currentIndex = 0;
+        this.currentSize = OS.size(this.currentSet);
+        return true;
+    }
+
+    constructor(private elements: AtomSetImpl) {
+        this.keyCount = elements.keys.length;
+        this.hasNext = this.keyCount > 0;
+        this.advance();
+    }
+}
+
+export function values(set: AtomSetImpl): Iterator<Atom> {
+    return new ElementsIterator(set);
+}
+
+export class TemplateAtomSetGenerator {
+    private keys: number[] = [];
+    private groups = IntMap.Mutable<AtomGroup>();
+    private templateGroups: IntMap<AtomGroup>;
+    private equalGroups = 0;
+
+    add(unit: number, set: OS) {
+        if (OS.size(set) === 0) return;
+        this.keys[this.keys.length] = unit;
+        if (this.templateGroups.has(unit)) {
+            const t = this.templateGroups.get(unit);
+            if (OS.areEqual(t.atoms, set)) {
+                this.groups.set(unit, t);
+                this.equalGroups++;
+            } else {
+                this.groups.set(unit, AtomGroup.createNew(set));
+            }
+        } else {
+            this.groups.set(unit, AtomGroup.createNew(set));
+        }
+    }
+
+    getSet(): AtomSetImpl {
+        if (this.equalGroups === this.template.keys.length && this.equalGroups === this.keys.length) {
+            return this.template;
+        }
+        return create(this.keys, this.groups);
+    }
+
+    constructor(private template: AtomSetImpl) {
+        this.templateGroups = template.groups;
+    }
+}
+
+export function TemplateGenerator(template: AtomSetImpl) {
+    return new TemplateAtomSetGenerator(template);
+}
+
+export class AtomSetGenerator {
+    private keys: number[] = [];
+    private groups = IntMap.Mutable<AtomGroup>();
+
+    add(unit: number, group: AtomGroup) {
+        if (AtomGroup.size(group) === 0) return;
+        this.keys[this.keys.length] = unit;
+        this.groups.set(unit, group);
+    }
+
+    getSet(): AtomSetImpl {
+        return create(this.keys, this.groups);
+    }
+}
+
+export function Generator() {
+    return new AtomSetGenerator();
+}
+
+/** When adding groups, compare them to existing ones. If they all match, return the whole original set. */
+class ChildGenerator {
+    private keys: number[] = [];
+    private groups = IntMap.Mutable<AtomGroup>();
+    private aEqual = 0;
+    private bEqual = 0;
+
+    add(unit: number, group: AtomGroup, a: AtomGroup, b: AtomGroup) {
+        if (AtomGroup.size(group) === 0) return;
+        if (a === group) this.aEqual++;
+        if (b === group) this.bEqual++;
+        this.keys[this.keys.length] = unit;
+        this.groups.set(unit, group);
+    }
+
+    addA(unit: number, group: AtomGroup, a: AtomGroup) {
+        if (AtomGroup.size(group) === 0) return;
+
+        if (a === group) this.aEqual++;
+        this.keys[this.keys.length] = unit;
+        this.groups.set(unit, group);
+    }
+
+    constructor(private a: AtomSetImpl, private b: AtomSetImpl) {
+    }
+
+    getSet(): AtomSetImpl {
+        if (this.aEqual === this.a.keys.length) return this.a;
+        if (this.bEqual === this.b.keys.length) return this.b;
+        return create(this.keys, this.groups);
+    }
+}
+
+function create(keys: number[], groups: IntMap<AtomGroup>): AtomSetImpl {
+    sortArray(keys);
+    let runningSize = 0;
+    const offsets = new Int32Array(keys.length + 1);
+    for (let i = 0, _i = keys.length; i < _i; i++) {
+        runningSize += AtomGroup.size(groups.get(keys[i]));
+        offsets[i + 1] = runningSize;
+    }
+    return { keys: SortedArray.ofSortedArray(keys), groups: IntMap.asImmutable(groups), offsets, hashCode: -1 };
+}
+
+function getUniqueElements(xs: number[]): number[] {
+    let count = 1;
+    for (let i = 1, _i = xs.length; i < _i; i++) {
+        if (xs[i - 1] !== xs[i]) count++;
+    }
+    const ret = new (xs as any).constructor(count);
+    ret[0] = xs[0];
+    let offset = 1;
+    for (let i = 1, _i = xs.length; i < _i; i++) {
+        if (xs[i - 1] !== xs[i]) ret[offset++] = xs[i];
+    }
+    return ret;
+}
+
+function normalizeArray(xs: number[]): number[] {
+    sortArray(xs);
+    for (let i = 1, _i = xs.length; i < _i; i++) {
+        if (xs[i - 1] === xs[i]) return getUniqueElements(xs);
+    }
+    return xs;
+}
+
+function ofAtomsImpl(xs: ArrayLike<Atom>, template: AtomSetImpl) {
+    if (xs.length === 0) return Empty;
+
+    const elements = IntMap.Mutable<number[]>();
+    const keys: number[] = [];
+    for (let i = 0, _i = xs.length; i < _i; i++) {
+        const x = xs[i];
+        const u = Atom.unit(x), v = Atom.index(x);
+        if (elements.has(u)) {
+            const set = elements.get(u);
+            set[set.length] = v;
+        } else {
+            keys[keys.length] = u;
+            elements.set(u, [v]);
+        }
+    }
+
+    const generator = TemplateGenerator(template);
+    for (let i = 0, _i = keys.length; i < _i; i++) {
+        const k = keys[i];
+        generator.add(k, OS.ofSortedArray(normalizeArray(elements.get(k))));
+    }
+
+    return generator.getSet();
+}
+
+function singletonImpl(atom: Atom, template: AtomSetImpl) {
+    const k = Atom.unit(atom), i = Atom.index(atom);
+    const { groups } = template;
+    const gs = IntMap.Mutable<AtomGroup>();
+    if (groups.has(k)) {
+        const g = groups.get(k);
+        if (AtomGroup.size(g) === 1 && AtomGroup.getAt(g, 0) === i) {
+            gs.set(k, g);
+            return create([k], gs);
+        }
+    }
+    gs.set(k, AtomGroup.createNew(OS.ofSingleton(i)));
+    return create([k], gs);
+}
+
+function getOffsetIndex(xs: ArrayLike<number>, value: number) {
+    let min = 0, max = xs.length - 1;
+    while (min < max) {
+        const mid = (min + max) >> 1;
+        const v = xs[mid];
+        if (value < v) max = mid - 1;
+        else if (value > v) min = mid + 1;
+        else return mid;
+    }
+    if (min > max) {
+        return max;
+    }
+    return value < xs[min] ? min - 1 : min;
+}
+
+function computeHash(set: AtomSetImpl) {
+    const { keys, groups } = set;
+    let hash = 23;
+    for (let i = 0, _i = keys.length; i < _i; i++) {
+        const k = keys[i];
+        hash = (31 * hash + k) | 0;
+        hash = (31 * hash + AtomGroup.hashCode(groups.get(k))) | 0;
+    }
+    hash = (31 * hash + size(set)) | 0;
+    hash = hash1(hash);
+    set.hashCode = hash;
+    return hash;
+}
+
+function findUnion(sets: ArrayLike<AtomSetImpl>, template: AtomSetImpl) {
+    if (!sets.length) return Empty;
+    if (sets.length === 1) return sets[0];
+    if (sets.length === 2 && sets[0] === sets[1]) return sets[0];
+
+    const keys: number[] = [];
+    const groups = IntMap.Mutable<AtomGroup>();
+    for (let i = 0, _i = sets.length; i < _i; i++) {
+        unionInto(keys, groups, sets[i]);
+    }
+
+    return normalizeUnion(keys, groups, template);
+}
+
+function normalizeUnion(keys: number[], groups: IntMap.Mutable<AtomGroup>, template: AtomSetImpl) {
+    let equalCount = 0;
+    let tg = template.groups, a: AtomGroup, t: AtomGroup;
+    for (let i = 0, _i = keys.length; i < _i; i++) {
+        const k = keys[i];
+        if (tg.has(k) && AtomGroup.areEqual(a = groups.get(k), t = tg.get(k))) {
+            groups.set(k, t);
+            equalCount++;
+        }
+    }
+    return equalCount === template.keys.length && equalCount === keys.length ? template : create(keys, groups);
+}
+
+function unionInto(keys: number[], groups: IntMap.Mutable<AtomGroup>, a: AtomSetImpl) {
+    const setKeys = a.keys;
+    const { groups: aG } = a;
+    for (let i = 0, _i = setKeys.length; i < _i; i++) {
+        const k = setKeys[i];
+        if (groups.has(k)) {
+            groups.set(k, AtomGroup.union(aG.get(k), groups.get(k)))
+        } else {
+            keys[keys.length] = k;
+            groups.set(k, aG.get(k));
+        }
+    }
+}

+ 18 - 11
src/mol-model/structure/structure/atom/set.ts

@@ -4,24 +4,26 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { OrderedSet, SortedArray, Iterator } from 'mol-data/int'
+import { SortedArray, Iterator, OrderedSet } from 'mol-data/int'
 import Atom from '../atom'
-import * as Impl from './set/impl'
-import createBuilder, { Builder as AtomSetBuilder } from './set/builder'
+import AtomGroup from './group'
+import * as Impl from './impl/set'
+import * as Builders from './impl/set-builder'
 
 /** A map-like representation of grouped atom set */
 namespace AtomSet {
     export const Empty: AtomSet = Impl.Empty as any;
 
-    export const create: (data: Atom | ArrayLike<Atom> | { [unitId: number]: OrderedSet }) => AtomSet = Impl.create as any;
+    export const ofAtoms: (atoms: ArrayLike<Atom>, template: AtomSet) => AtomSet = Impl.ofAtoms as any;
+    export const singleton: (atom: Atom, template: AtomSet) => AtomSet = Impl.singleton as any;
 
     export const unitCount: (set: AtomSet) => number = Impl.keyCount as any;
     export const unitIds: (set: AtomSet) => SortedArray = Impl.getKeys as any;
     export const unitHas: (set: AtomSet, id: number) => boolean = Impl.hasKey as any;
     export const unitGetId: (set: AtomSet, i: number) => number = Impl.getKey as any;
 
-    export const unitGetById: (set: AtomSet, key: number) => OrderedSet = Impl.getByKey as any;
-    export const unitGetByIndex: (set: AtomSet, i: number) => OrderedSet = Impl.getByIndex as any;
+    export const unitGetById: (set: AtomSet, key: number) => AtomGroup = Impl.getByKey as any;
+    export const unitGetByIndex: (set: AtomSet, i: number) => AtomGroup = Impl.getByIndex as any;
 
     export const atomCount: (set: AtomSet) => number = Impl.size as any;
     export const atomHas: (set: AtomSet, x: Atom) => boolean = Impl.hasAtom as any;
@@ -33,14 +35,19 @@ namespace AtomSet {
     export const areEqual: (a: AtomSet, b: AtomSet) => boolean = Impl.areEqual as any;
     export const areIntersecting: (a: AtomSet, b: AtomSet) => boolean = Impl.areIntersecting as any;
 
-    export const union: (a: AtomSet, b: AtomSet) => AtomSet = Impl.union as any;
-    export const unionMany: (sets: AtomSet[]) => AtomSet = Impl.unionMany as any;
+    export const union: (sets: ArrayLike<AtomSet>, template: AtomSet) => AtomSet = Impl.unionMany as any;
     export const intersect: (a: AtomSet, b: AtomSet) => AtomSet = Impl.intersect as any;
     export const subtract: (a: AtomSet, b: AtomSet) => AtomSet = Impl.subtract as any;
 
-    export type Builder = AtomSetBuilder
-    export function LinearBuilder(parent: AtomSet): Builder { return createBuilder(parent, true); }
-    export function UnsortedBuilder(parent: AtomSet): Builder { return createBuilder(parent, false); }
+    export type Builder = Builders.Builder
+    export const LinearBuilder = Builders.LinearBuilder
+    export const UnsortedBuilder = Builders.UnsortedBuilder
+
+    export interface Generator { add(unit: number, set: AtomGroup): void, getSet(): AtomSet }
+    export const Generator: () => Generator = Impl.Generator as any
+
+    export interface TemplateGenerator { add(unit: number, set: OrderedSet): void, getSet(): AtomSet }
+    export const TemplateGenerator: (template: AtomSet) => TemplateGenerator = Impl.TemplateGenerator as any
 
     // TODO: bounding sphere
     // TODO: distance, areWithIn?

+ 0 - 497
src/mol-model/structure/structure/atom/set/impl.ts

@@ -1,497 +0,0 @@
-/**
- * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- */
-
-import { SortedArray, Interval, Iterator, OrderedSet } from 'mol-data/int'
-import { sortArray } from 'mol-data/util/sort'
-import { hash1 } from 'mol-data/util/hash-functions'
-import Atom from '../../atom'
-
-/** Long and painful implementation starts here */
-
-export interface AtomSetElements { [id: number]: OrderedSet, offsets: number[], hashCode: number, keys: SortedArray }
-export type AtomSetImpl = Atom | AtomSetElements
-
-export const Empty: AtomSetImpl = { offsets: [0], hashCode: 0, keys: SortedArray.Empty };
-
-export function create(data: Atom | ArrayLike<Atom> | { [id: number]: OrderedSet }): AtomSetImpl {
-    if (typeof data === 'number' || Atom.is(data)) return data;
-    if (isArrayLike(data)) return ofAtoms(data);
-    return ofObject(data as { [id: number]: OrderedSet });
-}
-
-export function isSingleton(set: AtomSetImpl) {
-    return typeof set === 'number';
-}
-
-export function getKeys(set: AtomSetImpl): SortedArray {
-    if (typeof set === 'number') return SortedArray.ofSingleton(set);
-    return (set as AtomSetElements).keys;
-}
-
-export function keyCount(set: AtomSetImpl): number {
-    if (typeof set === 'number') return 1;
-    return (set as AtomSetElements).keys.length;
-}
-
-export function hasKey(set: AtomSetImpl, key: number): boolean {
-    if (typeof set === 'number') return Atom.unit(set) === key;
-    return !!(set as AtomSetElements)[key]
-}
-
-export function getKey(set: AtomSetImpl, index: number): number {
-    if (typeof set === 'number') return Atom.unit(set);
-    return (set as AtomSetElements).keys[index];
-}
-
-export function hasAtom(set: AtomSetImpl, t: Atom): boolean {
-    if (typeof set === 'number') return Atom.areEqual(t, set);
-    const os = (set as AtomSetElements)[Atom.unit(t)];
-    return !!os && OrderedSet.has(os, Atom.index(t));
-}
-
-export function getByKey(set: AtomSetImpl, key: number): OrderedSet {
-    if (typeof set === 'number') {
-        return Atom.unit(set) === key ? OrderedSet.ofSingleton(Atom.index(set)) : OrderedSet.Empty;
-    }
-    return (set as AtomSetElements)[key] || OrderedSet.Empty;
-}
-
-export function getByIndex(set: AtomSetImpl, index: number): OrderedSet {
-    if (typeof set === 'number') return index === 0 ? OrderedSet.ofSingleton(Atom.index(set)) : OrderedSet.Empty;
-    const key = (set as AtomSetElements).keys[index];
-    return (set as AtomSetElements)[key] || OrderedSet.Empty;
-}
-
-export function getAt(set: AtomSetImpl, i: number): Atom {
-    if (typeof set === 'number') return set;
-    return getAtE(set as AtomSetElements, i);
-}
-
-export function indexOf(set: AtomSetImpl, t: Atom) {
-    if (typeof set === 'number') return Atom.areEqual(set, t) ? 0 : -1;
-    return indexOfE(set as AtomSetElements, t);
-}
-
-/** Number elements in the "child" sets */
-export function size(set: AtomSetImpl) {
-    if (typeof set === 'number') return 1;
-    return (set as AtomSetElements).offsets[(set as AtomSetElements).offsets.length - 1];
-}
-
-export function hashCode(set: AtomSetImpl) {
-    if (typeof set === 'number') return Atom.hashCode(set);
-    if ((set as AtomSetElements).hashCode !== -1) return (set as AtomSetElements).hashCode;
-    return computeHash((set as AtomSetElements));
-}
-
-export function areEqual(a: AtomSetImpl, b: AtomSetImpl) {
-    if (typeof a === 'number') {
-        if (typeof b === 'number') return Atom.areEqual(a, b);
-        return false;
-    }
-    if (typeof b === 'number') return false;
-    return areEqualEE(a as AtomSetElements, b as AtomSetElements);
-}
-
-export function areIntersecting(a: AtomSetImpl, b: AtomSetImpl) {
-    if (typeof a === 'number') {
-        if (typeof b === 'number') return Atom.areEqual(a, b);
-        return areIntersectingNE(a, b as AtomSetElements);
-    }
-    if (typeof b === 'number') return areIntersectingNE(b, a as AtomSetElements);
-    return areIntersectingEE(a as AtomSetElements, b as AtomSetElements);
-}
-
-export function intersect(a: AtomSetImpl, b: AtomSetImpl) {
-    if (typeof a === 'number') {
-        if (typeof b === 'number') return Atom.areEqual(a, b) ? a : Empty;
-        return intersectNE(a, b as AtomSetElements);
-    }
-    if (typeof b === 'number') return intersectNE(b, a as AtomSetElements);
-    return intersectEE(a as AtomSetElements, b as AtomSetElements);
-}
-
-export function subtract(a: AtomSetImpl, b: AtomSetImpl) {
-    if (typeof a === 'number') {
-        if (typeof b === 'number') return Atom.areEqual(a, b) ? Empty : a;
-        return subtractNE(a, b as AtomSetElements);
-    }
-    if (typeof b === 'number') return subtractEN(a as AtomSetElements, b);
-    return subtractEE(a as AtomSetElements, b as AtomSetElements);
-}
-
-export function union(a: AtomSetImpl, b: AtomSetImpl) {
-    return findUnion([a, b]);
-}
-
-export function unionMany(sets: ArrayLike<AtomSetImpl>) {
-    return findUnion(sets);
-}
-
-class ElementsIterator implements Iterator<Atom> {
-    private unit: number = 0;
-    private keyCount: number;
-    private setIndex = -1;
-    private currentIndex = 0;
-    private currentSize = 0;
-    private currentSet: OrderedSet = OrderedSet.Empty;
-
-    hasNext: boolean = false;
-
-    move() {
-        if (!this.hasNext) return Atom.Zero;
-        const ret = Atom.create(this.unit, OrderedSet.getAt(this.currentSet, this.currentIndex++));
-        if (this.currentIndex >= this.currentSize) this.advance();
-        return ret;
-    }
-
-    private advance() {
-        if (++this.setIndex >= this.keyCount) {
-            this.hasNext = false;
-            return false;
-        }
-        this.unit = this.elements.keys[this.setIndex];
-        this.currentSet = this.elements[this.unit];
-        this.currentIndex = 0;
-        this.currentSize = OrderedSet.size(this.currentSet);
-        return true;
-    }
-
-    constructor(private elements: AtomSetElements) {
-        this.keyCount = elements.keys.length;
-        this.hasNext = this.keyCount > 0;
-        this.advance();
-    }
-}
-
-export function values(set: AtomSetImpl): Iterator<Atom> {
-    if (typeof set === 'number') return Iterator.Value(set as Atom);
-    return new ElementsIterator(set as AtomSetElements);
-}
-
-function isArrayLike(x: any): x is ArrayLike<Atom> {
-    return x && (typeof x.length === 'number' && (Array.isArray(x) || !!x.buffer));
-}
-
-function ofObject(data: { [id: number]: OrderedSet }) {
-    const keys = [];
-
-    const _keys = Object.keys(data);
-    for (let i = 0, _i = _keys.length; i < _i; i++) {
-        const k = +_keys[i];
-        if (OrderedSet.size(data[k]) > 0) keys[keys.length] = k;
-    }
-    if (!keys.length) return Empty;
-    if (keys.length === 1) {
-        const set = data[keys[0]];
-        if (OrderedSet.size(set) === 1) return Atom.create(keys[0], OrderedSet.getAt(set, 0));
-    }
-    return ofObject1(keys, data);
-}
-
-function ofObject1(keys: number[], data: { [id: number]: OrderedSet }) {
-    if (keys.length === 1) {
-        const k = keys[0];
-        const set = data[k];
-        if (OrderedSet.size(set) === 1) return Atom.create(k, OrderedSet.getAt(set, 0));
-    }
-    sortArray(keys);
-    return _createObjectOrdered(SortedArray.ofSortedArray(keys), data);
-}
-
-function ofObjectOrdered(keys: SortedArray, data: { [id: number]: OrderedSet }) {
-    if (keys.length === 1) {
-        const k = keys[0];
-        const set = data[k];
-        if (OrderedSet.size(set) === 1) return Atom.create(k, OrderedSet.getAt(set, 0));
-    }
-    return _createObjectOrdered(keys, data);
-}
-
-function _createObjectOrdered(keys: SortedArray, data: { [id: number]: OrderedSet }) {
-    const ret: AtomSetElements = Object.create(null);
-    ret.keys = keys;
-    const offsets = [0];
-    let runningSize = 0;
-    for (let i = 0, _i = keys.length; i < _i; i++) {
-        const k = keys[i];
-        const set = data[k];
-        ret[k] = set;
-        runningSize += OrderedSet.size(set);
-        offsets[offsets.length] = runningSize;
-    }
-    ret.offsets = offsets;
-    ret.hashCode = -1;
-    return ret;
-}
-
-function getUniqueElements(xs: number[]) {
-    let count = 1;
-    for (let i = 1, _i = xs.length; i < _i; i++) {
-        if (xs[i - 1] !== xs[i]) count++;
-    }
-    const ret = new (xs as any).constructor(count);
-    ret[0] = xs[0];
-    let offset = 1;
-    for (let i = 1, _i = xs.length; i < _i; i++) {
-        if (xs[i - 1] !== xs[i]) ret[offset++] = xs[i];
-    }
-    return ret;
-}
-
-function normalizeArray(xs: number[]) {
-    sortArray(xs);
-    for (let i = 1, _i = xs.length; i < _i; i++) {
-        if (xs[i - 1] === xs[i]) return getUniqueElements(xs);
-    }
-    return xs;
-}
-
-function ofAtoms(xs: ArrayLike<Atom>) {
-    if (xs.length === 0) return Empty;
-    const sets: { [key: number]: number[] } = Object.create(null);
-    for (let i = 0, _i = xs.length; i < _i; i++) {
-        const x = xs[i];
-        const u = Atom.unit(x), v = Atom.index(x);
-        const set = sets[u];
-        if (set) set[set.length] = v;
-        else sets[u] = [v];
-    }
-    const ret: { [key: number]: OrderedSet } = Object.create(null);
-    const keys = [];
-    const _keys = Object.keys(sets);
-    for (let i = 0, _i = _keys.length; i < _i; i++) {
-        const k = +_keys[i];
-        keys[keys.length] = k;
-        ret[k] = OrderedSet.ofSortedArray(normalizeArray(sets[k]));
-    }
-    return ofObject1(keys, ret);
-}
-
-function getOffsetIndex(xs: ArrayLike<number>, value: number) {
-    let min = 0, max = xs.length - 1;
-    while (min < max) {
-        const mid = (min + max) >> 1;
-        const v = xs[mid];
-        if (value < v) max = mid - 1;
-        else if (value > v) min = mid + 1;
-        else return mid;
-    }
-    if (min > max) {
-        return max;
-    }
-    return value < xs[min] ? min - 1 : min;
-}
-
-function getAtE(set: AtomSetElements, i: number): Atom {
-    const { offsets, keys } = set;
-    const o = getOffsetIndex(offsets, i);
-    if (o >= offsets.length - 1) return 0 as any;
-    const k = keys[o];
-    const e = OrderedSet.getAt(set[k], i - offsets[o]);
-    return Atom.create(k, e);
-}
-
-function indexOfE(set: AtomSetElements, t: Atom) {
-    const { keys } = set;
-    const u = Atom.unit(t);
-    const setIdx = SortedArray.indexOf(keys, u);
-    if (setIdx < 0) return -1;
-    const o = OrderedSet.indexOf(set[u], Atom.index(t));
-    if (o < 0) return -1;
-    return set.offsets[setIdx] + o;
-}
-
-function computeHash(set: AtomSetElements) {
-    const { keys } = set;
-    let hash = 23;
-    for (let i = 0, _i = keys.length; i < _i; i++) {
-        const k = keys[i];
-        hash = (31 * hash + k) | 0;
-        hash = (31 * hash + OrderedSet.hashCode(set[k])) | 0;
-    }
-    hash = (31 * hash + size(set)) | 0;
-    hash = hash1(hash);
-    set.hashCode = hash;
-    return hash;
-}
-
-function areEqualEE(a: AtomSetElements, b: AtomSetElements) {
-    if (a === b) return true;
-    if (size(a) !== size(a)) return false;
-
-    const keys = a.keys;
-    if (!SortedArray.areEqual(keys, b.keys)) return false;
-    for (let i = 0, _i = keys.length; i < _i; i++) {
-        const k = keys[i];
-        if (!OrderedSet.areEqual(a[k], b[k])) return false;
-    }
-    return true;
-}
-
-function areIntersectingNE(a: Atom, b: AtomSetElements) {
-    const u = Atom.unit(a);
-    return SortedArray.has(b.keys, u) && OrderedSet.has(b[u], Atom.index(a));
-}
-
-function areIntersectingEE(a: AtomSetElements, b: AtomSetElements) {
-    if (a === b) return true;
-    const keysA = a.keys, keysB = b.keys;
-    if (!SortedArray.areIntersecting(a.keys, b.keys)) return false;
-    const r = SortedArray.findRange(keysA, SortedArray.min(keysB), SortedArray.max(keysB));
-    const start = Interval.start(r), end = Interval.end(r);
-    for (let i = start; i < end; i++) {
-        const k = keysA[i];
-        const ak = a[k], bk = b[k];
-        if (!!ak && !!bk && OrderedSet.areIntersecting(ak, bk)) return true;
-    }
-    return false;
-}
-
-function intersectNE(a: Atom, b: AtomSetElements) {
-    const u = Atom.unit(a);
-    return !!b[u] && OrderedSet.has(b[u], Atom.index(a)) ? a : Empty;
-}
-
-function intersectEE(a: AtomSetElements, b: AtomSetElements) {
-    if (a === b) return a;
-
-    const keysA = a.keys, keysB = b.keys;
-    if (!SortedArray.areIntersecting(a.keys, b.keys)) return Empty;
-    const r = SortedArray.findRange(keysA, SortedArray.min(keysB), SortedArray.max(keysB));
-    const start = Interval.start(r), end = Interval.end(r);
-
-    const keys = [], ret = Object.create(null);
-    for (let i = start; i < end; i++) {
-        const k = keysA[i];
-        const bk = b[k];
-        if (!bk) continue;
-        const intersection = OrderedSet.intersect(a[k], b[k]);
-        if (OrderedSet.size(intersection) > 0) {
-            keys[keys.length] = k;
-            ret[k] = intersection;
-        }
-    }
-    return ofObjectOrdered(SortedArray.ofSortedArray(keys), ret);
-}
-
-function subtractNE(a: Atom, b: AtomSetElements) {
-    return hasAtom(b, a) ? Empty : a;
-}
-
-function subtractEN(a: AtomSetElements, b: Atom): AtomSetImpl {
-    if (!hasAtom(a, b)) return a;
-
-    const u = Atom.unit(b), v = Atom.index(b);
-    const set = a[u];
-    if (OrderedSet.size(set) === 1) {
-        // remove the entire unit.
-        return ofObjectOrdered(SortedArray.subtract(a.keys, SortedArray.ofSingleton(u)), a);
-    } else {
-        const ret: { [key: number]: OrderedSet } = Object.create(null);
-        for (let i = 0, _i = a.keys.length; i < _i; i++) {
-            const k = a.keys[i];
-            if (k === u) {
-                ret[k] = OrderedSet.subtract(set, OrderedSet.ofSingleton(v));
-            } else ret[k] = a[k];
-        }
-        return ofObjectOrdered(a.keys, ret);
-    }
-}
-
-function subtractEE(a: AtomSetElements, b: AtomSetElements) {
-    if (a === b) return Empty;
-
-    const keysA = a.keys, keysB = b.keys;
-    if (!SortedArray.areIntersecting(keysA, keysB)) return a;
-    const r = SortedArray.findRange(keysA, SortedArray.min(keysB), SortedArray.max(keysB));
-    const start = Interval.start(r), end = Interval.end(r);
-
-    const keys = [], ret = Object.create(null);
-    for (let i = 0; i < start; i++) {
-        const k = keysA[i];
-        keys[keys.length] = k;
-        ret[k] = a[k];
-    }
-    for (let i = start; i < end; i++) {
-        const k = keysA[i];
-        const ak = a[k], bk = b[k];
-        if (!!bk) {
-            const subtraction = OrderedSet.subtract(ak, bk);
-            if (OrderedSet.size(subtraction) > 0) {
-                keys[keys.length] = k;
-                ret[k] = subtraction;
-            }
-        } else {
-            keys[keys.length] = k;
-            ret[k] = a[k];
-        }
-    }
-    for (let i = end, _i = keysA.length; i < _i; i++) {
-        const k = keysA[i];
-        keys[keys.length] = k;
-        ret[k] = a[k];
-    }
-    return ofObjectOrdered(SortedArray.ofSortedArray(keys), ret);
-}
-
-function findUnion(sets: ArrayLike<AtomSetImpl>) {
-    if (!sets.length) return Empty;
-    if (sets.length === 1) return sets[0];
-    if (sets.length === 2 && areEqual(sets[0], sets[1])) return sets[0];
-
-    const eCount = { count: 0 };
-    const ns = unionN(sets, eCount);
-    if (!eCount.count) return ns;
-    const ret = Object.create(null);
-    for (let i = 0, _i = sets.length; i < _i; i++) {
-        const s = sets[i];
-        if (typeof s !== 'number') unionInto(ret, s as AtomSetElements);
-    }
-    if (size(ns as AtomSetImpl) > 0) {
-        if (typeof ns === 'number') unionIntoN(ret, ns as any);
-        else unionInto(ret, ns as AtomSetElements);
-    }
-    return ofObject(ret);
-}
-
-function unionN(sets: ArrayLike<AtomSetImpl>, eCount: { count: number }) {
-    let countN = 0, countE = 0;
-    for (let i = 0, _i = sets.length; i < _i; i++) {
-        if (typeof sets[i] === 'number') countN++;
-        else countE++;
-    }
-    eCount.count = countE;
-    if (!countN) return Empty;
-    if (countN === sets.length) return ofAtoms(sets as ArrayLike<Atom>);
-    const packed = Atom.createEmptyArray(countN);
-    let offset = 0;
-    for (let i = 0, _i = sets.length; i < _i; i++) {
-        const s = sets[i];
-        if (typeof s === 'number') packed[offset++] = s;
-    }
-    return ofAtoms(packed as any);
-}
-
-function unionInto(data: { [key: number]: OrderedSet }, a: AtomSetElements) {
-    const keys = a.keys;
-    for (let i = 0, _i = keys.length; i < _i; i++) {
-        const k = keys[i];
-        const set = data[k];
-        if (set) data[k] = OrderedSet.union(set, a[k]);
-        else data[k] = a[k];
-    }
-}
-
-function unionIntoN(data: { [key: number]: OrderedSet }, a: Atom) {
-    const u = Atom.unit(a);
-    const set = data[u];
-    if (set) {
-        data[u] = OrderedSet.union(set, OrderedSet.ofSingleton(Atom.index(a)));
-    } else {
-        data[u] = OrderedSet.ofSingleton(Atom.index(a));
-    }
-}

+ 26 - 22
src/mol-model/structure/structure/structure.ts

@@ -5,25 +5,24 @@
  */
 
 import { OrderedSet, Iterator } from 'mol-data/int'
-import UniqueArray from 'mol-data/util/unique-array'
+import { UniqueArray } from 'mol-data/util'
 import SymmetryOperator from 'mol-math/geometry/symmetry-operator'
 import { Model, Format } from '../model'
 import Unit from './unit'
 import AtomSet from './atom/set'
+import AtomGroup from './atom/group'
 import Atom from './atom'
 
-
-interface Structure extends Readonly<{
-    units: { readonly [id: number]: Unit },
-    atoms: AtomSet
-}> { }
+// A structure is a pair of "units" and an atom set.
+// Each unit contains the data and transformation of its corresponding atoms.
+interface Structure {
+    readonly units: ReadonlyArray<Unit>,
+    readonly atoms: AtomSet
+}
 
 namespace Structure {
-    export const Empty = { units: {}, atoms: AtomSet.Empty };
-
-    export function create(units: Structure['units'], atoms: AtomSet): Structure {
-        return { units, atoms };
-    }
+    export function create(units: ReadonlyArray<Unit>, atoms: AtomSet): Structure { return { units, atoms }; }
+    export function Empty(units: ReadonlyArray<Unit>): Structure { return create(units, AtomSet.Empty); };
 
     export function ofData(format: Format) {
         const models = Model.create(format);
@@ -35,29 +34,32 @@ namespace Structure {
         const builder = Builder();
 
         for (let c = 0; c < chains.count; c++) {
-            const unit = Unit.create(model, SymmetryOperator.Default);
-            builder.addUnit(unit);
-            builder.addAtoms(unit.id, OrderedSet.ofBounds(chains.segments[c], chains.segments[c + 1]));
+            const group = AtomGroup.createNew(OrderedSet.ofBounds(chains.segments[c], chains.segments[c + 1]));
+            const unit = Unit.create(model, SymmetryOperator.Default, group);
+            builder.add(unit, unit.fullGroup);
         }
 
         return builder.getStructure();
     }
 
     export interface Builder {
+        add(unit: Unit, atoms: AtomGroup): void,
         addUnit(unit: Unit): void,
-        addAtoms(unitId: number, atoms: OrderedSet): void,
+        setAtoms(unitId: number, atoms: AtomGroup): void,
         getStructure(): Structure,
         readonly atomCount: number
     }
 
     class BuilderImpl implements Builder {
-        private units = Object.create(null);
-        private atoms = Object.create(null);
+        private _unitId = 0;
+        private units: Unit[] = [];
+        private atoms = AtomSet.Generator();
         atomCount = 0;
 
-        addUnit(unit: Unit) { this.units[unit.id] = unit; }
-        addAtoms(unitId: number, atoms: OrderedSet) { this.atoms[unitId] = atoms; this.atomCount += OrderedSet.size(atoms); }
-        getStructure(): Structure { return this.atomCount > 0 ? Structure.create(this.units, AtomSet.create(this.atoms)) : Empty; }
+        add(unit: Unit, atoms: AtomGroup) { const id = this.addUnit(unit); this.setAtoms(id, atoms); }
+        addUnit(unit: Unit) { const id = this._unitId++; this.units[id] = unit; return id; }
+        setAtoms(unitId: number, atoms: AtomGroup) { this.atoms.add(unitId, atoms); this.atomCount += AtomGroup.size(atoms); }
+        getStructure(): Structure { return this.atomCount > 0 ? Structure.create(this.units, this.atoms.getSet()) : Empty(this.units); }
     }
 
     export function Builder(): Builder { return new BuilderImpl(); }
@@ -70,9 +72,11 @@ namespace Structure {
     }
 
     export function getModels(s: Structure) {
+        const { units, atoms } = s;
         const arr = UniqueArray.create<Model['id'], Model>();
-        for (const k of Object.keys(s.units)) {
-            const u = s.units[+k];
+        const ids = AtomSet.unitIds(atoms);
+        for (let i = 0; i < ids.length; i++) {
+            const u = units[ids[i]];
             UniqueArray.add(arr, u.model.id, u.model);
         }
         return arr.array;

+ 44 - 0
src/mol-model/structure/structure/symmetry.ts

@@ -0,0 +1,44 @@
+/**
+ * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import Structure from './structure'
+import AtomSet from './atom/set'
+import Unit from './unit'
+import { Selection } from '../query'
+import { ModelSymmetry } from '../model'
+
+namespace Symmetry {
+    export const  buildAssembly = buildAssemblyImpl;
+}
+
+export default Symmetry;
+
+function buildAssemblyImpl(structure: Structure, name: string) {
+    const models = Structure.getModels(structure);
+    if (models.length !== 1) throw new Error('Can only build assemblies from structures based on 1 model.');
+
+    const assembly = ModelSymmetry.findAssembly(models[0], name);
+    if (!assembly) throw new Error(`Assembly '${name}' is not defined.`);
+
+    const assembler = Structure.Builder();
+
+    for (const g of assembly.operatorGroups) {
+        const selection = g.selector(structure);
+        if (Selection.structureCount(selection) === 0) continue;
+        const { units, atoms } = Selection.unionStructure(selection);
+
+        const unitIds = AtomSet.unitIds(atoms);
+
+        for (const oper of g.operators) {
+            for (let uI = 0, _uI = unitIds.length; uI < _uI; uI++) {
+                const unit = units[unitIds[uI]];
+                assembler.add(Unit.withOperator(unit, oper), AtomSet.unitGetByIndex(atoms, uI));
+            }
+        }
+    }
+
+    return assembler.getStructure();
+}

+ 21 - 16
src/mol-model/structure/structure/unit.ts

@@ -5,18 +5,26 @@
  */
 
 import SymmetryOperator from 'mol-math/geometry/symmetry-operator'
+import AtomGroup from './atom/group'
 import { Model } from '../model'
 
+// A bulding block of a structure that corresponds
+// to a "natural group of atoms" (most often a "chain")
+// together with a tranformation (rotation and translation)
+// that is dynamically applied to the underlying atom set.
+//
+// An atom set can be referenced by multiple diffrent units which
+// makes construction of assemblies and spacegroups very efficient.
 interface Unit extends SymmetryOperator.ArrayMapping {
-    // Structure-level unique identifier of the unit.
-    readonly id: number,
-
     // Provides access to the underlying data.
     readonly model: Model,
 
-    // Determines the operation applied to this unit.
-    // The transform and and inverse are baked into the "getPosition" function
-    readonly operator: SymmetryOperator,
+    // The "full" atom group corresponding to this unit.
+    // Every selection is a subset of this atoms group.
+    // Things like inter-unit bonds or spatial lookups
+    // can be be implemented efficiently as "views" of the
+    // full group.
+    readonly fullGroup: AtomGroup,
 
     // Reference some commonly accessed things for faster access.
     readonly residueIndex: ArrayLike<number>,
@@ -28,14 +36,14 @@ interface Unit extends SymmetryOperator.ArrayMapping {
 }
 
 namespace Unit {
-    export function create(model: Model, operator: SymmetryOperator): Unit {
+    export function create(model: Model, operator: SymmetryOperator, fullGroup: AtomGroup): Unit {
         const h = model.hierarchy;
         const { invariantPosition, position, x, y, z } = SymmetryOperator.createMapping(operator, model.conformation);
 
         return {
-            id: nextUnitId(),
             model,
             operator,
+            fullGroup,
             residueIndex: h.residueSegments.segmentMap,
             chainIndex: h.chainSegments.segmentMap,
             hierarchy: model.hierarchy,
@@ -45,13 +53,10 @@ namespace Unit {
             x, y, z
         };
     }
-}
 
-export default Unit;
+    export function withOperator(unit: Unit, operator: SymmetryOperator) {
+        return create(unit.model, SymmetryOperator.compose(unit.operator, operator), unit.fullGroup);
+    }
+}
 
-let _id = 0;
-function nextUnitId() {
-    const ret = _id;
-    _id = (_id + 1) % 0x3fffffff;
-    return ret;
-}
+export default Unit;

+ 2 - 0
src/mol-ql/TODO

@@ -0,0 +1,2 @@
+- Have the ability to define schemas for output. 
+  Define properties for each pattern/whatever that could be exported as CIF/CSV/JSON.

+ 24 - 9
src/mol-util/computation.ts

@@ -124,8 +124,6 @@ class ObservableContext implements Computation.Context {
     private observers: Computation.ProgressObserver[] | undefined = void 0;
     private progress: Computation.Progress = { message: 'Working...', current: 0, max: 0, elapsedMs: 0, isIndeterminate: true, requestAbort: void 0 };
 
-    lastDelta = 0;
-
     private checkAborted() {
         if (this.abortRequested) throw Computation.Aborted;
     }
@@ -186,7 +184,6 @@ class ObservableContext implements Computation.Context {
             }
         }
 
-        this.lastDelta = time - this.lastUpdated;
         this.lastUpdated = time;
 
         return Scheduler.immediatePromise();
@@ -223,11 +220,13 @@ class ChunkerImpl implements Computation.Chunker {
     private processedSinceUpdate = 0;
     private updater: Computation.Context['update'];
 
-    private computeChunkSize() {
-        const lastDelta = (this.context as ObservableContext).lastDelta || 0;
-        if (!lastDelta) return this.nextChunkSize;
+    private computeChunkSize(delta: number) {
+        if (!delta) {
+            this.processedSinceUpdate = 0;
+            return this.nextChunkSize;
+        }
         const rate = (this.context as ObservableContext).updateRate || DefaulUpdateRateMs;
-        const ret = Math.round(this.processedSinceUpdate * rate / lastDelta + 1);
+        const ret = Math.round(this.processedSinceUpdate * rate / delta + 1);
         this.processedSinceUpdate = 0;
         return ret;
     }
@@ -245,18 +244,34 @@ class ChunkerImpl implements Computation.Chunker {
 
     async process(nextChunk: (size: number) => number, update: (updater: Computation.Context['update']) => Promise<void> | void, nextChunkSize?: number) {
         if (typeof nextChunkSize !== 'undefined') this.setNextChunkSize(nextChunkSize);
+        this.processedSinceUpdate = 0;
 
+        // track time for the actual computation and exclude the "update time"
+        let chunkStart = Computation.now();
         let lastChunkSize: number;
+        let chunkCount = 0;
+        let totalSize = 0;
+        let updateCount = 0;
         while ((lastChunkSize = nextChunk(this.getNextChunkSize())) > 0) {
+            chunkCount++;
             this.processedSinceUpdate += lastChunkSize;
+            totalSize += lastChunkSize;
             if (this.context.requiresUpdate) {
+                let time = Computation.now();
                 await update(this.updater);
-                this.nextChunkSize = this.computeChunkSize();
+                this.nextChunkSize = updateCount > 0
+                    ? Math.round((totalSize + this.computeChunkSize(time - chunkStart)) / (chunkCount + 1))
+                    : this.computeChunkSize(time - chunkStart)
+                updateCount++;
+                chunkStart = Computation.now();
             }
         }
         if (this.context.requiresUpdate) {
+            let time = Computation.now();
             await update(this.updater);
-            this.nextChunkSize = this.computeChunkSize();
+            this.nextChunkSize = updateCount > 0
+                ? Math.round((totalSize + this.computeChunkSize(time - chunkStart)) / (chunkCount + 1))
+                : this.computeChunkSize(time - chunkStart)
         }
     }
 

+ 5 - 2
src/mol-util/scheduler.ts

@@ -89,7 +89,7 @@ function createImmediateActions() {
     function canUsePostMessage() {
         // The test against `importScripts` prevents this implementation from being installed inside a web worker,
         // where `global.postMessage` means something completely different and can't be used for this purpose.
-        const global = window as any;
+        const global = typeof window !== 'undefined' ? window as any : void 0;
         if (global && global.postMessage && !global.importScripts) {
             let postMessageIsAsynchronous = true;
             const oldOnMessage = global.onmessage;
@@ -108,6 +108,7 @@ function createImmediateActions() {
         // * http://www.whatwg.org/specs/web-apps/current-work/multipage/comms.html#crossDocumentMessages
 
         const messagePrefix = 'setImmediate$' + Math.random() + '$';
+        const global = typeof window !== 'undefined' ? window as any : void 0;
         const onGlobalMessage = function(event: any) {
             if (event.source === global &&
                 typeof event.data === 'string' &&
@@ -187,7 +188,9 @@ function createImmediateActions() {
 
 const immediateActions = (function () {
     if (typeof setImmediate !== 'undefined') {
-        return { setImmediate, clearImmediate };
+        if (typeof window !== 'undefined') {
+            return { setImmediate: (handler: any, ...args: any[]) => window.setImmediate(handler, ...args as any), clearImmediate: (handle: any) => window.clearImmediate(handle) };
+        } else return { setImmediate, clearImmediate }
     }
     return createImmediateActions();
 }());

+ 1 - 1
src/perf-tests/chunked-array-vs-native.ts

@@ -1,5 +1,5 @@
 import * as B from 'benchmark'
-import ChunkedArray from 'mol-data/util/chunked-array'
+import { ChunkedArray } from 'mol-data/util'
 
 function testNative(size: number) {
     const xs = new Array(size);

+ 192 - 79
src/perf-tests/sets.ts

@@ -2,84 +2,84 @@ import * as B from 'benchmark'
 import { Tuple, Segmentation, OrderedSet as OrdSet } from 'mol-data/int'
 import { AtomSet } from 'mol-model/structure'
 
-export namespace Iteration {
-    const U = 1000, V = 2500;
-
-    const control: number[] = [];
-    const sets = Object.create(null);
-    for (let i = 1; i < U; i++) {
-        const set = [];
-        for (let j = 1; j < V; j++) {
-            control[control.length] = j * j + 1;
-            set[set.length] = j * j + 1;
-        }
-        sets[i * i] = OrdSet.ofSortedArray(set);
-    }
-    const ms = AtomSet.create(sets);
-
-    export function native() {
-        let s = 0;
-        for (let i = 0, _i = control.length; i < _i; i++) s += control[i];
-        return s;
-    }
-
-    export function iterators() {
-        let s = 0;
-        const it = AtomSet.atoms(ms);
-        while (it.hasNext) {
-            const v = it.move();
-            s += Tuple.snd(v);
-        }
-        return s;
-    }
-
-    export function elementAt() {
-        let s = 0;
-        for (let i = 0, _i = AtomSet.atomCount(ms); i < _i; i++) s += Tuple.snd(AtomSet.atomGetAt(ms, i));
-        return s;
-    }
-
-    export function manual() {
-        let s = 0;
-        const keys = AtomSet.unitIds(ms);
-        for (let i = 0, _i = OrdSet.size(keys); i < _i; i++) {
-            const set = AtomSet.unitGetById(ms, OrdSet.getAt(keys, i));
-            for (let j = 0, _j = OrdSet.size(set); j < _j; j++) {
-                s += OrdSet.getAt(set, j);
-            }
-        }
-        return s;
-    }
-
-    export function manual1() {
-        let s = 0;
-        for (let i = 0, _i = AtomSet.unitCount(ms); i < _i; i++) {
-            const set = AtomSet.unitGetByIndex(ms, i);
-            for (let j = 0, _j = OrdSet.size(set); j < _j; j++) {
-                s += OrdSet.getAt(set, j);
-            }
-        }
-        return s;
-    }
-
-    export function run() {
-        const suite = new B.Suite();
-
-        // const values: number[] = [];
-        // for (let i = 0; i < 1000000; i++) values[i] = (Math.random() * 1000) | 0;
-
-        console.log(Iteration.native(), Iteration.iterators(), Iteration.elementAt(), Iteration.manual(), Iteration.manual1());
-
-        suite
-            .add('native', () => Iteration.native())
-            .add('iterators', () => Iteration.iterators())
-            .add('manual', () => Iteration.manual())
-            .add('manual1', () => Iteration.manual1())
-            .add('el at', () => Iteration.elementAt())
-            .on('cycle', (e: any) => console.log(String(e.target)))
-            .run();
-    }
-}
+// export namespace Iteration {
+//     const U = 1000, V = 2500;
+
+//     const control: number[] = [];
+//     const sets = Object.create(null);
+//     for (let i = 1; i < U; i++) {
+//         const set = [];
+//         for (let j = 1; j < V; j++) {
+//             control[control.length] = j * j + 1;
+//             set[set.length] = j * j + 1;
+//         }
+//         sets[i * i] = OrdSet.ofSortedArray(set);
+//     }
+//     const ms = AtomSet.create(sets);
+
+//     export function native() {
+//         let s = 0;
+//         for (let i = 0, _i = control.length; i < _i; i++) s += control[i];
+//         return s;
+//     }
+
+//     export function iterators() {
+//         let s = 0;
+//         const it = AtomSet.atoms(ms);
+//         while (it.hasNext) {
+//             const v = it.move();
+//             s += Tuple.snd(v);
+//         }
+//         return s;
+//     }
+
+//     export function elementAt() {
+//         let s = 0;
+//         for (let i = 0, _i = AtomSet.atomCount(ms); i < _i; i++) s += Tuple.snd(AtomSet.atomGetAt(ms, i));
+//         return s;
+//     }
+
+//     export function manual() {
+//         let s = 0;
+//         const keys = AtomSet.unitIds(ms);
+//         for (let i = 0, _i = OrdSet.size(keys); i < _i; i++) {
+//             const set = AtomSet.unitGetById(ms, OrdSet.getAt(keys, i));
+//             for (let j = 0, _j = OrdSet.size(set); j < _j; j++) {
+//                 s += OrdSet.getAt(set, j);
+//             }
+//         }
+//         return s;
+//     }
+
+//     export function manual1() {
+//         let s = 0;
+//         for (let i = 0, _i = AtomSet.unitCount(ms); i < _i; i++) {
+//             const set = AtomSet.unitGetByIndex(ms, i);
+//             for (let j = 0, _j = OrdSet.size(set); j < _j; j++) {
+//                 s += OrdSet.getAt(set, j);
+//             }
+//         }
+//         return s;
+//     }
+
+//     export function run() {
+//         const suite = new B.Suite();
+
+//         // const values: number[] = [];
+//         // for (let i = 0; i < 1000000; i++) values[i] = (Math.random() * 1000) | 0;
+
+//         console.log(Iteration.native(), Iteration.iterators(), Iteration.elementAt(), Iteration.manual(), Iteration.manual1());
+
+//         suite
+//             .add('native', () => Iteration.native())
+//             .add('iterators', () => Iteration.iterators())
+//             .add('manual', () => Iteration.manual())
+//             .add('manual1', () => Iteration.manual1())
+//             .add('el at', () => Iteration.elementAt())
+//             .on('cycle', (e: any) => console.log(String(e.target)))
+//             .run();
+//     }
+// }
 
 export namespace Union {
     function createArray(n: number) {
@@ -331,7 +331,120 @@ export namespace ObjectVsMap {
     }
 }
 
-ObjectVsMap.run();
+export namespace IntVsStringIndices {
+    type WithKeys<K> = { keys: K[], data: { [key: number]: number } }
+    type MapWithKeys = { keys: number[], map: Map<number, number> }
+
+    function createCacheKeys(n: number): WithKeys<number> {
+        const data = Object.create(null), keys = [];
+        data.__ = void 0;
+        delete data.__;
+        for (let i = 1; i <= n; i++) {
+            const k = i * (i + 1);
+            keys[keys.length] = k;
+            data[k] = i + 1;
+        }
+        return { data, keys };
+    }
+
+    function createMapKeys(n: number): MapWithKeys {
+        const map = new Map<number, number>(), keys = [];
+        for (let i = 1; i <= n; i++) {
+            const k = i * (i + 1);
+            keys[keys.length] = k;
+            map.set(k, i + 1);
+        }
+        return { map, keys };
+    }
+
+    export function createInt(n: number) {
+        const ret = Object.create(null);
+        ret.__ = void 0;
+        delete ret.__;
+        for (let i = 1; i <= n; i++) ret[i * (i + 1)] = i + 1;
+        return ret;
+    }
+
+    export function createStr(n: number) {
+        const ret = Object.create(null);
+        ret.__ = void 0;
+        delete ret.__;
+        for (let i = 1; i <= n; i++) ret['' + (i * (i + 1))] = i + 1;
+        return ret;
+    }
+
+    export function createMap(n: number) {
+        const map = new Map<number, number>();
+        for (let i = 1; i <= n; i++) map.set(i * (i + 1), i + 1);
+        return map;
+    }
+
+    export function sumInt(xs: { [key: number]: number }) {
+        let s = 0;
+        const keys = Object.keys(xs);
+        for (let i = 0, _i = keys.length; i < _i; i++) s += xs[+keys[i]];
+        return s;
+    }
+
+    export function sumStr(xs: { [key: string]: number }) {
+        let s = 0;
+        const keys = Object.keys(xs);
+        for (let i = 0, _i = keys.length; i < _i; i++) s += xs[keys[i]];
+        return s;
+    }
+
+    export function sumMap(map: Map<number, number>) {
+        let s = 0;
+        const values = map.keys();
+        while (true) {
+            const { done, value } = values.next();
+            if (done) break;
+            s += value;
+        }
+        return s;
+    }
+
+    export function sumCached(xs: WithKeys<number>) {
+        let s = 0;
+        const keys = xs.keys, data = xs.data;
+        for (let i = 0, _i = keys.length; i < _i; i++) s += data[keys[i]];
+        return s;
+    }
+
+    export function sumKeyMap(xs: MapWithKeys) {
+        let s = 0;
+        const keys = xs.keys, map = xs.map;
+        for (let i = 0, _i = keys.length; i < _i; i++) s += map.get(keys[i])!;
+        return s;
+    }
+
+    export function run() {
+        const N = 1000;
+        //const int = createInt(N);
+        const map = createMap(N);
+        //const str = createStr(N);
+        const keys = createCacheKeys(N);
+        const keyMap = createMapKeys(N);
+        console.log(sumMap(map), sumCached(keys), sumKeyMap(keyMap));
+        new B.Suite()
+            //.add('c int', () => createInt(N))
+            .add('q map', () => sumMap(map))
+            .add('c map', () => createMap(N))
+            .add('c mk', () => createMapKeys(N))
+            //.add('c str', () => createStr(N))
+            .add('c cc', () => createCacheKeys(N))
+            //.add('q int', () => sumInt(int))
+            .add('q mk', () => sumKeyMap(keyMap))
+            //.add('q str', () => sumStr(str))
+            .add('q cc', () => sumCached(keys))
+            .on('cycle', (e: any) => console.log(String(e.target)))
+            .run();
+    }
+}
+
+IntVsStringIndices.run();
+
+//ObjectVsMap.run();
 
 //testSegments();
 

+ 49 - 17
src/perf-tests/structure.ts

@@ -10,8 +10,8 @@ import * as util from 'util'
 import * as fs from 'fs'
 import CIF from 'mol-io/reader/cif'
 
-import { Structure, Model, Queries as Q, Atom, AtomSet, Selection } from 'mol-model/structure'
-import { OrderedSet as OrdSet, Segmentation } from 'mol-data/int'
+import { Structure, Model, Queries as Q, Atom, AtomGroup, AtomSet, Selection, Symmetry } from 'mol-model/structure'
+import { Segmentation } from 'mol-data/int'
 
 import to_mmCIF from 'mol-model/structure/export/mmcif'
 
@@ -29,6 +29,21 @@ async function readData(path: string) {
     }
 }
 
+function *test() {
+    yield 10;
+    return 15;
+}
+
+async function runIt<T>(itP: () => IterableIterator<T>) {
+    const it = itP();
+    while (true) {
+        const { value, done } = it.next();
+        if (done) return value;
+    }
+}
+
+runIt(test).then(r => console.log('rerdasdasda', r))
+
 export async function readCIF(path: string) {
     console.time('readData');
     const input = await readData(path)
@@ -77,8 +92,9 @@ export namespace PropertyAccess {
             l.unit = units[unitIds[i]];
             const set = AtomSet.unitGetByIndex(atoms, i);
 
-            for (let j = 0, _j = OrdSet.size(set); j < _j; j++) {
-                l.atom = OrdSet.getAt(set, j);
+
+            for (let j = 0, _j = AtomGroup.size(set); j < _j; j++) {
+                l.atom = AtomGroup.getAt(set, j);
                 s += p(l);
             }
         }
@@ -99,20 +115,20 @@ export namespace PropertyAccess {
             l.unit = unit;
             const set = AtomSet.unitGetByIndex(atoms, i);
 
-            const chainsIt = Segmentation.transientSegments(unit.hierarchy.chainSegments, set);
+            const chainsIt = Segmentation.transientSegments(unit.hierarchy.chainSegments, set.atoms);
             const residues = unit.hierarchy.residueSegments;
             while (chainsIt.hasNext) {
                 cC++;
 
                 const chainSegment = chainsIt.move();
-                const residuesIt = Segmentation.transientSegments(residues, set, chainSegment);
+                const residuesIt = Segmentation.transientSegments(residues, set.atoms, chainSegment);
                 while (residuesIt.hasNext) {
                     rC++;
                     const residueSegment = residuesIt.move();
                     // l.atom = OrdSet.getAt(set, residueSegment.start);
                     // console.log(unit.hierarchy.residues.auth_comp_id.value(unit.residueIndex[l.atom]), l.atom, OrdSet.getAt(set, residueSegment.end))
                     for (let j = residueSegment.start, _j = residueSegment.end; j < _j; j++) {
-                        l.atom = OrdSet.getAt(set, j);
+                        l.atom = AtomGroup.getAt(set, j);
                         vA++;
                         s += p(l);
                     }
@@ -136,9 +152,9 @@ export namespace PropertyAccess {
             const unit = units[unitIds[i]];
             l.unit = unit;
             const set = AtomSet.unitGetByIndex(atoms, i);
-            const residuesIt = Segmentation.transientSegments(unit.hierarchy.residueSegments, set);
+            const residuesIt = Segmentation.transientSegments(unit.hierarchy.residueSegments, set.atoms);
             while (residuesIt.hasNext) {
-                l.atom = OrdSet.getAt(set, residuesIt.move().start);
+                l.atom = AtomGroup.getAt(set, residuesIt.move().start);
                 s += p(l);
             }
         }
@@ -203,8 +219,8 @@ export namespace PropertyAccess {
             const set = AtomSet.unitGetByIndex(atoms, i);
             //const { residueIndex, chainIndex } = unit;
             const p = unit.conformation.atomId.value;
-            for (let j = 0, _j = OrdSet.size(set); j < _j; j++) {
-                const aI = OrdSet.getAt(set, j);
+            for (let j = 0, _j = AtomGroup.size(set); j < _j; j++) {
+                const aI = AtomGroup.getAt(set, j);
                 s += p(aI);
             }
         }
@@ -241,13 +257,29 @@ export namespace PropertyAccess {
         console.log(to_mmCIF('test', s));
     }
 
+    export function testAssembly(id: string, s: Structure) {
+        console.time('assembly')
+        const a = Symmetry.buildAssembly(s, '1');
+        console.timeEnd('assembly')
+        fs.writeFileSync(`e:/test/molstar/${id}_assembly.bcif`, to_mmCIF(id, a, true));
+        console.log('exported');
+        //write(a);
+    }
+
     export async function run() {
-        const { structures, models, mmcif } = await readCIF('./examples/1cbs_full.bcif');
-        //const { structures, models, mmcif } = await readCIF('e:/test/quick/3j3q_full.bcif');
+        //const { structures, models, mmcif } = await readCIF('./examples/1cbs_full.bcif');
+        const { structures, models } = await readCIF('e:/test/quick/3j3q_full.bcif');
         //const { structures, models, mmcif } = await readCIF('e:/test/quick/1cbs_updated.cif');
+        //const { structures, models/*, mmcif*/ } = await readCIF('e:/test/quick/5j7v_updated.cif');
+
+        //console.log(mmcif.pdbx_struct_oper_list.matrix.toArray());
+        // console.log(mmcif.pdbx_struct_oper_list.vector.toArray());
+
+        // testAssembly('5j7v', structures[0]);
+        // throw '';
+
+        // console.log(models[0].symmetry.assemblies);
 
-        console.log(mmcif.pdbx_struct_oper_list.matrix.toArray());
-        console.log(mmcif.pdbx_struct_oper_list.vector.toArray());
 
         //const { structures, models } = await readCIF('e:/test/molstar/3j3q.bcif');
 
@@ -258,8 +290,8 @@ export namespace PropertyAccess {
 
         // return;
 
-        console.log(baseline(models[0]));
-        console.log(sumProperty(structures[0], l => l.unit.model.conformation.atomId.value(l.atom)));
+        console.log('bs', baseline(models[0]));
+        console.log('sp', sumProperty(structures[0], l => l.unit.model.conformation.atomId.value(l.atom)));
         console.log(sumPropertySegmented(structures[0], l => l.unit.model.conformation.atomId.value(l.atom)));
 
         //console.log(sumPropertySegmentedMutable(structures[0], l => l.unit.model.conformation.atomId.value(l.atom)));

+ 2 - 1
src/script.ts

@@ -148,13 +148,14 @@ async function runCIF(input: string | Uint8Array) {
 export async function _cif() {
     let path = `./examples/1grm_updated.cif`;
     // path = '../test/3j3q.cif'  // lets have a relative path for big test files
+    // path = 'e:/test/quick/3j3q_updated.cif';
     const input = await readFileAsync(path, 'utf8')
     console.log('------------------');
     console.log('Text CIF:');
     runCIF(input);
 
     path = `./examples/1cbs_full.bcif`;
-    // const path = 'c:/test/quick/3j3q.cif';
+    
     const input2 = await readFileAsync(path)
     console.log('------------------');
     console.log('BinaryCIF:');