From 90077b5789784290b5de68c1712c599315a48b23 Mon Sep 17 00:00:00 2001
From: David Sehnal <david.sehnal@gmail.com>
Date: Fri, 10 Nov 2017 12:43:53 +0100
Subject: [PATCH] mol-data refactoring, add IntMap

---
 src/mol-data/_spec/equiv-index.spec.ts        |   2 +-
 src/mol-data/db/column.ts                     |   5 +
 src/mol-data/db/table.ts                      |  20 +++
 src/mol-data/int.ts                           |   3 +-
 src/mol-data/int/map.ts                       |  45 +++++++
 src/mol-data/util.ts                          |  13 +-
 src/mol-data/util/array.ts                    |  25 ++++
 src/mol-data/util/chunked-array.ts            |   2 +-
 src/mol-data/util/equivalence-classes.ts      |   4 +-
 src/mol-data/util/grouping.ts                 |  26 ++--
 src/mol-data/util/hash-set.ts                 |   6 +-
 src/mol-data/util/unique-array.ts             |   2 +-
 src/mol-io/common/binary-cif/array-encoder.ts |   2 +-
 .../structure/model/properties/symmetry.ts    |   7 +-
 src/mol-model/structure/query/selection.ts    |   2 +-
 .../structure/structure/structure.ts          |   2 +-
 src/perf-tests/chunked-array-vs-native.ts     |   2 +-
 src/perf-tests/sets.ts                        | 115 +++++++++++++++++-
 18 files changed, 245 insertions(+), 38 deletions(-)
 create mode 100644 src/mol-data/int/map.ts
 create mode 100644 src/mol-data/util/array.ts

diff --git a/src/mol-data/_spec/equiv-index.spec.ts b/src/mol-data/_spec/equiv-index.spec.ts
index 9083c0145..339022a96 100644
--- a/src/mol-data/_spec/equiv-index.spec.ts
+++ b/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', () => {
diff --git a/src/mol-data/db/column.ts b/src/mol-data/db/column.ts
index 54fe666de..adcd28cc2 100644
--- a/src/mol-data/db/column.ts
+++ b/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);
diff --git a/src/mol-data/db/table.ts b/src/mol-data/db/table.ts
index d01629bf1..6335e37ed 100644
--- a/src/mol-data/db/table.ts
+++ b/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
\ No newline at end of file
diff --git a/src/mol-data/int.ts b/src/mol-data/int.ts
index 54c2795bb..24b005bd2 100644
--- a/src/mol-data/int.ts
+++ b/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 }
\ No newline at end of file
+export { Interval, OrderedSet, Segmentation, SortedArray, Tuple, LinkedIndex, IntMap, Iterator }
\ No newline at end of file
diff --git a/src/mol-data/int/map.ts b/src/mol-data/int/map.ts
new file mode 100644
index 000000000..1ef86a532
--- /dev/null
+++ b/src/mol-data/int/map.ts
@@ -0,0 +1,45 @@
+/**
+ * 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'
+
+/** Immutable by convention IntMap */
+interface IntMap<T> {
+    keys(): IterableIterator<number>,
+    values(): IterableIterator<T>,
+    get(key: number): T,
+    readonly size: number
+}
+
+namespace IntMap {
+    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 ofObj<T>(obj: { [key: number]: T }): IntMap<T> {
+        const keys = Object.keys(obj);
+        const ret = new Map<number, T>();
+        for (let i = 0, _i = keys.length; i < _i; i++) {
+            const k = keys[i];
+            ret.set(+k, obj[k as any]);
+        }
+        return ret as IntMap<T>;
+    }
+
+    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 default IntMap;
\ No newline at end of file
diff --git a/src/mol-data/util.ts b/src/mol-data/util.ts
index ab0c73fcf..629b3b517 100644
--- a/src/mol-data/util.ts
+++ b/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 }
\ No newline at end of file
+export * from './util/grouping'
+export * from './util/array'
\ No newline at end of file
diff --git a/src/mol-data/util/array.ts b/src/mol-data/util/array.ts
new file mode 100644
index 000000000..664138327
--- /dev/null
+++ b/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;
+}
\ No newline at end of file
diff --git a/src/mol-data/util/chunked-array.ts b/src/mol-data/util/chunked-array.ts
index 0664a4c12..f9b977571 100644
--- a/src/mol-data/util/chunked-array.ts
+++ b/src/mol-data/util/chunked-array.ts
@@ -134,4 +134,4 @@ namespace ChunkedArray {
     }
 }
 
-export default ChunkedArray
\ No newline at end of file
+export { ChunkedArray }
\ No newline at end of file
diff --git a/src/mol-data/util/equivalence-classes.ts b/src/mol-data/util/equivalence-classes.ts
index 936d39b29..cc5f4af62 100644
--- a/src/mol-data/util/equivalence-classes.ts
+++ b/src/mol-data/util/equivalence-classes.ts
@@ -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;
\ No newline at end of file
diff --git a/src/mol-data/util/grouping.ts b/src/mol-data/util/grouping.ts
index 213f3cf54..71e9ce5a0 100644
--- a/src/mol-data/util/grouping.ts
+++ b/src/mol-data/util/grouping.ts
@@ -4,31 +4,34 @@
  * @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> {
-    private byKey = new Map<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.byKey.has(key)) {
-            const group = this.byKey.get(key)!;
+        if (!!this.map.has(key)) {
+            const group = this.map.get(key)!;
             group[group.length] = a;
         } else {
             const group = [a];
-            this.byKey.set(key, group);
+            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 };
+        return { keys: this.keys, groups: this.groups, map: this.map };
     }
 
     constructor(private getKey: (v: V) => K) { }
@@ -38,10 +41,13 @@ export function Grouper<V, K>(getKey: (x: V) => K) {
     return new GroupingImpl<K, V>(getKey);
 }
 
-function groupBy<V, K>(values: ArrayLike<V>, getKey: (x: V) => K) {
+export function groupBy<V, K>(values: ArrayLike<V> | Column<V>, getKey: (x: V) => K) {
     const gs = Grouper(getKey);
-    for (let i = 0, _i = values.length; i < _i; i++) gs.add(values[i]);
+    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();
-}
-
-export default groupBy;
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/src/mol-data/util/hash-set.ts b/src/mol-data/util/hash-set.ts
index 297b8eeb6..0e41ad62f 100644
--- a/src/mol-data/util/hash-set.ts
+++ b/src/mol-data/util/hash-set.ts
@@ -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;
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/src/mol-data/util/unique-array.ts b/src/mol-data/util/unique-array.ts
index f628baa96..5a5caa501 100644
--- a/src/mol-data/util/unique-array.ts
+++ b/src/mol-data/util/unique-array.ts
@@ -21,4 +21,4 @@ namespace UniqueArray {
     }
 }
 
-export default UniqueArray
\ No newline at end of file
+export { UniqueArray }
\ No newline at end of file
diff --git a/src/mol-io/common/binary-cif/array-encoder.ts b/src/mol-io/common/binary-cif/array-encoder.ts
index 344171b8a..67a9084ca 100644
--- a/src/mol-io/common/binary-cif/array-encoder.ts
+++ b/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 {
diff --git a/src/mol-model/structure/model/properties/symmetry.ts b/src/mol-model/structure/model/properties/symmetry.ts
index 38303de0e..dfe599c2a 100644
--- a/src/mol-model/structure/model/properties/symmetry.ts
+++ b/src/mol-model/structure/model/properties/symmetry.ts
@@ -5,6 +5,7 @@
  */
 
 import SymmetryOperator from 'mol-math/geometry/symmetry-operator'
+import { arrayFind } from 'mol-data/util'
 import { Query } from '../../query'
 import { Model } from '../../model'
 
@@ -47,12 +48,8 @@ namespace Symmetry {
     export const Empty: Symmetry = { assemblies: [] };
 
     export function findAssembly(model: Model, id: string): Assembly | undefined {
-        const { assemblies } = model.symmetry;
         const _id = id.toLocaleLowerCase();
-        for (let i = 0; i < assemblies.length; i++) {
-            if (assemblies[i].id.toLowerCase() === _id) return assemblies[i];
-        }
-        return void 0;
+        return arrayFind(model.symmetry.assemblies, a => a.id.toLowerCase() === _id);
     }
 }
 
diff --git a/src/mol-model/structure/query/selection.ts b/src/mol-model/structure/query/selection.ts
index 8504e4252..c1e0f9fb1 100644
--- a/src/mol-model/structure/query/selection.ts
+++ b/src/mol-model/structure/query/selection.ts
@@ -5,7 +5,7 @@
  */
 
 import Iterator from 'mol-data/iterator'
-import HashSet from 'mol-data/util/hash-set'
+import { HashSet } from 'mol-data/util'
 import { Structure, Atom, AtomSet } from '../structure'
 
 type Selection =
diff --git a/src/mol-model/structure/structure/structure.ts b/src/mol-model/structure/structure/structure.ts
index 317ea029f..8d5af1dcb 100644
--- a/src/mol-model/structure/structure/structure.ts
+++ b/src/mol-model/structure/structure/structure.ts
@@ -5,7 +5,7 @@
  */
 
 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'
diff --git a/src/perf-tests/chunked-array-vs-native.ts b/src/perf-tests/chunked-array-vs-native.ts
index 1b145425d..e7843412a 100644
--- a/src/perf-tests/chunked-array-vs-native.ts
+++ b/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);
diff --git a/src/perf-tests/sets.ts b/src/perf-tests/sets.ts
index 699c9037c..07bb5919a 100644
--- a/src/perf-tests/sets.ts
+++ b/src/perf-tests/sets.ts
@@ -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();
 
-- 
GitLab