Browse Source

wip, interactions

Alexander Rose 5 years ago
parent
commit
8fd56dbc38

+ 233 - 0
src/mol-model-props/computed/chemistry/functional-group.ts

@@ -0,0 +1,233 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Structure, Unit } from '../../../mol-model/structure';
+import { StructureElement } from '../../../mol-model/structure/structure';
+import { Elements, isHalogen } from '../../../mol-model/structure/model/properties/atomic/types';
+import { ElementSymbol, LinkType } from '../../../mol-model/structure/model/types';
+import { eachBondedAtom, bondCount, typeSymbol, bondToElementCount } from './util';
+
+function isAromatic(unit: Unit.Atomic, index: StructureElement.UnitIndex) {
+    // TODO also extend unit.rings with geometry/composition-based aromaticity detection and use it here in addition
+    const { offset, edgeProps } = unit.links
+    for (let i = offset[index], il = offset[index + 1]; i < il; ++i) {
+        if (LinkType.is(LinkType.Flag.Aromatic, edgeProps.flags[i])) return true
+    }
+    return false
+}
+
+function bondToCarbonylCount(structure: Structure, unit: Unit.Atomic, index: StructureElement.UnitIndex) {
+    let carbonylCount = 0
+    eachBondedAtom(structure, unit, index, (unit: Unit.Atomic, index: StructureElement.UnitIndex) => {
+        if (isCarbonyl(structure, unit, index)) carbonylCount += 1
+    })
+    return carbonylCount
+}
+
+//
+
+/**
+ * Nitrogen in a quaternary amine
+ */
+export function isQuaternaryAmine(structure: Structure, unit: Unit.Atomic, index: StructureElement.UnitIndex) {
+    return (
+        typeSymbol(unit, index) === Elements.N &&
+        bondCount(structure, unit, index) === 4 &&
+        bondToElementCount(structure, unit, index, Elements.H) === 0
+    )
+}
+
+/**
+ * Nitrogen in a tertiary amine
+ */
+export function isTertiaryAmine(structure: Structure, unit: Unit.Atomic, index: StructureElement.UnitIndex, idealValence: number) {
+    return (
+        typeSymbol(unit, index) === Elements.N &&
+        bondCount(structure, unit, index) === 4 &&
+        idealValence === 3
+    )
+}
+
+/**
+ * Nitrogen in an imide
+ */
+export function isImide(structure: Structure, unit: Unit.Atomic, index: StructureElement.UnitIndex) {
+    let flag = false
+    if (typeSymbol(unit, index) === Elements.N &&
+        (bondCount(structure, unit, index) - bondToElementCount(structure, unit, index, Elements.H)) === 2
+    ) {
+        flag = bondToCarbonylCount(structure, unit, index) === 2
+    }
+    return flag
+}
+
+/**
+ * Nitrogen in an amide
+ */
+export function isAmide(structure: Structure, unit: Unit.Atomic, index: StructureElement.UnitIndex) {
+    let flag = false
+    if (typeSymbol(unit, index) === Elements.N &&
+        (bondCount(structure, unit, index) - bondToElementCount(structure, unit, index, Elements.H)) === 2
+    ) {
+        flag = bondToCarbonylCount(structure, unit, index) === 1
+    }
+    return flag
+}
+
+/**
+ * Sulfur in a sulfonium group
+ */
+export function isSulfonium(structure: Structure, unit: Unit.Atomic, index: StructureElement.UnitIndex) {
+    return (
+        typeSymbol(unit, index) === Elements.S &&
+        bondCount(structure, unit, index) === 3 &&
+        bondToElementCount(structure, unit, index, Elements.H) === 0
+    )
+}
+
+/**
+ * Sulfur in a sulfonic acid or sulfonate group
+ */
+export function isSulfonicAcid(structure: Structure, unit: Unit.Atomic, index: StructureElement.UnitIndex) {
+    return (
+        typeSymbol(unit, index) === Elements.S &&
+        bondToElementCount(structure, unit, index, Elements.O) === 3
+    )
+}
+
+/**
+ * Sulfur in a sulfate group
+ */
+export function isSulfate(structure: Structure, unit: Unit.Atomic, index: StructureElement.UnitIndex) {
+    return (
+        typeSymbol(unit, index) === Elements.S &&
+        bondToElementCount(structure, unit, index, Elements.O) === 4
+    )
+}
+
+/**
+ * Phosphor in a phosphate group
+ */
+export function isPhosphate (structure: Structure, unit: Unit.Atomic, index: StructureElement.UnitIndex) {
+    return (
+        typeSymbol(unit, index) === Elements.P &&
+        bondToElementCount(structure, unit, index, Elements.O) === bondCount(structure, unit, index)
+    )
+}
+
+/**
+ * Halogen with one bond to a carbon
+ */
+export function isHalocarbon (structure: Structure, unit: Unit.Atomic, index: StructureElement.UnitIndex) {
+    return (
+        isHalogen(typeSymbol(unit, index)) &&
+        bondCount(structure, unit, index) === 1 &&
+        bondToElementCount(structure, unit, index, Elements.C) === 1
+    )
+}
+
+/**
+ * Carbon in a carbonyl/acyl group
+ *
+ * TODO currently only checks intra bonds for group detection
+ */
+export function isCarbonyl(structure: Structure, unit: Unit.Atomic, index: StructureElement.UnitIndex) {
+    let flag = false
+    if (typeSymbol(unit, index) === Elements.C) {
+        const { offset, edgeProps, b } = unit.links
+        for (let i = offset[index], il = offset[index + 1]; i < il; ++i) {
+            if (edgeProps.order[i] === 2 && typeSymbol(unit, b[i] as StructureElement.UnitIndex) === Elements.O) {
+                flag = true
+                break
+            }
+        }
+    }
+    return flag
+}
+
+/**
+ * Carbon in a carboxylate group
+ */
+export function isCarboxylate (structure: Structure, unit: Unit.Atomic, index: StructureElement.UnitIndex) {
+    let terminalOxygenCount = 0
+    if (
+        typeSymbol(unit, index) === Elements.C &&
+        bondToElementCount(structure, unit, index, Elements.O) === 2 &&
+        bondToElementCount(structure, unit, index, Elements.C) === 1
+    ) {
+        eachBondedAtom(structure, unit, index, (unit: Unit.Atomic, index: StructureElement.UnitIndex) => {
+            if (
+                typeSymbol(unit, index) === Elements.O &&
+                bondCount(structure, unit, index) - bondToElementCount(structure, unit, index, Elements.H) === 1
+            ) {
+                terminalOxygenCount += 1
+            }
+        })
+    }
+    return terminalOxygenCount === 2
+}
+
+/**
+ * Carbon in a guanidine group
+ */
+export function isGuanidine (structure: Structure, unit: Unit.Atomic, index: StructureElement.UnitIndex) {
+    let terminalNitrogenCount = 0
+    if (
+        typeSymbol(unit, index) === Elements.C &&
+        bondCount(structure, unit, index) === 3 &&
+        bondToElementCount(structure, unit, index, Elements.N) === 3
+    ) {
+        eachBondedAtom(structure, unit, index, (unit: Unit.Atomic, index: StructureElement.UnitIndex) => {
+            if (
+                bondCount(structure, unit, index) - bondToElementCount(structure, unit, index, Elements.H) === 1
+            ) {
+                terminalNitrogenCount += 1
+            }
+        })
+    }
+    return terminalNitrogenCount === 2
+}
+
+/**
+ * Carbon in a acetamidine group
+ */
+export function isAcetamidine (structure: Structure, unit: Unit.Atomic, index: StructureElement.UnitIndex) {
+    let terminalNitrogenCount = 0
+    if (
+        typeSymbol(unit, index) === Elements.C &&
+        bondCount(structure, unit, index) === 3 &&
+        bondToElementCount(structure, unit, index, Elements.N) === 2 &&
+        bondToElementCount(structure, unit, index, Elements.C) === 1
+    ) {
+        eachBondedAtom(structure, unit, index, (unit: Unit.Atomic, index: StructureElement.UnitIndex) => {
+            if (
+                bondCount(structure, unit, index) - bondToElementCount(structure, unit, index, Elements.H) === 1
+            ) {
+                terminalNitrogenCount += 1
+            }
+        })
+    }
+    return terminalNitrogenCount === 2
+}
+
+const PolarElements = new Set<ElementSymbol>([ 'N', 'O', 'S', 'F', 'CL', 'BR', 'I' ] as ElementSymbol[])
+export function isPolar(element: ElementSymbol) { return PolarElements.has(element) }
+
+export function hasPolarNeighbour (structure: Structure, unit: Unit.Atomic, index: StructureElement.UnitIndex) {
+    let flag = false
+    eachBondedAtom(structure, unit, index, (unit: Unit.Atomic, index: StructureElement.UnitIndex) => {
+        if (isPolar(typeSymbol(unit, index))) flag = true
+    })
+    return flag
+}
+
+export function hasAromaticNeighbour (structure: Structure, unit: Unit.Atomic, index: StructureElement.UnitIndex) {
+    let flag = false
+    eachBondedAtom(structure, unit, index, (unit: Unit.Atomic, index: StructureElement.UnitIndex) => {
+        if (isAromatic(unit, index)) flag = true
+    })
+    return flag
+}

+ 117 - 0
src/mol-model-props/computed/chemistry/geometry.ts

@@ -0,0 +1,117 @@
+/**
+ * Copyright (c) 2017-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Fred Ludlow <Fred.Ludlow@astx.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { degToRad } from '../../../mol-math/misc';
+import { Vec3 } from '../../../mol-math/linear-algebra';
+import { Structure, Unit, StructureElement } from '../../../mol-model/structure';
+import { eachBondedAtom, typeSymbol } from './util';
+import { Elements } from '../../../mol-model/structure/model/properties/atomic/types';
+
+/**
+ * Numbering mostly inline with coordination number from VSEPR,
+ * breaks with `SquarePlanar = 7`
+ */
+export const enum AtomGeometry {
+    Spherical = 0,
+    Terminal = 1,
+    Linear = 2,
+    Trigonal = 3,
+    Tetrahedral = 4,
+    TrigonalBiPyramidal = 5,
+    Octahedral = 6,
+    SquarePlanar = 7, // Okay, it breaks down somewhere!
+    Unknown = 8
+}
+
+export function assignGeometry (totalCoordination: number): AtomGeometry {
+    switch (totalCoordination) {
+        case 0: return AtomGeometry.Spherical
+        case 1: return AtomGeometry.Terminal
+        case 2: return AtomGeometry.Linear
+        case 3: return AtomGeometry.Trigonal
+        case 4: return AtomGeometry.Tetrahedral
+        default: return AtomGeometry.Unknown
+
+    }
+}
+
+export const AtomGeometryAngles = new Map<AtomGeometry, number>([
+    [ AtomGeometry.Linear, degToRad(180) ],
+    [ AtomGeometry.Trigonal, degToRad(120) ],
+    [ AtomGeometry.Tetrahedral, degToRad(109.4721) ],
+    [ AtomGeometry.Octahedral, degToRad(90) ]
+])
+
+// tmp objects for `calcAngles` and `calcPlaneAngle`
+const tmpDir1 = Vec3()
+const tmpDir2 = Vec3()
+const tmpPosA = Vec3()
+const tmpPosB = Vec3()
+const tmpPosX = Vec3()
+
+/**
+ * Calculate the angles x-a1-a2 for all x where x is a heavy atom (not H) bonded to ap1.
+ */
+export function calcAngles (structure: Structure, unitA: Unit.Atomic, indexA: StructureElement.UnitIndex, unitB: Unit.Atomic, indexB: StructureElement.UnitIndex): number[] {
+    const angles: number[] = []
+    unitA.conformation.position(unitA.elements[indexA], tmpPosA)
+    unitB.conformation.position(unitB.elements[indexB], tmpPosB)
+    Vec3.sub(tmpDir1, tmpPosB, tmpPosA)
+
+    eachBondedAtom(structure, unitA, indexA, (unitX: Unit.Atomic, indexX: StructureElement.UnitIndex) => {
+        if (typeSymbol(unitX, indexX) !== Elements.H) {
+            unitX.conformation.position(unitX.elements[indexX], tmpPosX)
+            Vec3.sub(tmpDir2, tmpPosX, tmpPosA)
+            angles.push(Vec3.angle(tmpDir1, tmpDir2))
+        }
+    })
+    return angles
+}
+
+/**
+ * Find two neighbours of ap1 to define a plane (if possible) and
+ * measure angle out of plane to ap2
+ * @param  {AtomProxy} ap1 First atom (angle centre)
+ * @param  {AtomProxy} ap2 Second atom (out-of-plane)
+ * @return {number}        Angle from plane to second atom
+ */
+export function calcPlaneAngle (structure: Structure, unitA: Unit.Atomic, indexA: StructureElement.UnitIndex, unitB: Unit.Atomic, indexB: StructureElement.UnitIndex): number | undefined {
+    unitA.conformation.position(unitA.elements[indexA], tmpPosA)
+    unitB.conformation.position(unitB.elements[indexB], tmpPosB)
+    Vec3.sub(tmpDir1, tmpPosB, tmpPosA)
+
+    const neighbours = [Vec3(), Vec3()]
+    let ni = 0
+    let unitX1: Unit.Atomic | undefined
+    let indexX1: StructureElement.UnitIndex | undefined
+    eachBondedAtom(structure, unitA, indexA, (unitX: Unit.Atomic, indexX: StructureElement.UnitIndex) => {
+        if (ni > 1) return
+        if (typeSymbol(unitX, indexX) !== Elements.H) {
+            unitX1 = unitX
+            indexX1 = indexX
+            unitX.conformation.position(unitX.elements[indexX], tmpPosX)
+            Vec3.sub(neighbours[ni++], tmpPosX, tmpPosA)
+        }
+    })
+    if (ni === 1 && unitX1 && indexX1) {
+        eachBondedAtom(structure, unitX1, indexX1, (unitX: Unit.Atomic, indexX: StructureElement.UnitIndex) => {
+            if (ni > 1) return
+            if (unitX === unitA && indexX === indexA) return
+            if (typeSymbol(unitX, indexX) !== Elements.H) {
+                unitX.conformation.position(unitX.elements[indexX], tmpPosX)
+                Vec3.sub(neighbours[ni++], tmpPosX, tmpPosA)
+            }
+        })
+    }
+
+    if (ni !== 2) {
+        return
+    }
+
+    Vec3.cross(tmpDir2, neighbours[0], neighbours[1])
+    return Math.abs((Math.PI / 2) - Vec3.angle(tmpDir2, tmpDir1))
+}

+ 86 - 0
src/mol-model-props/computed/chemistry/util.ts

@@ -0,0 +1,86 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Structure, Unit } from '../../../mol-model/structure';
+import { StructureElement } from '../../../mol-model/structure/structure';
+import { Elements } from '../../../mol-model/structure/model/properties/atomic/types';
+
+export function typeSymbol(unit: Unit.Atomic, index: StructureElement.UnitIndex) {
+    return unit.model.atomicHierarchy.atoms.type_symbol.value(unit.elements[index])
+}
+
+export function formalCharge(unit: Unit.Atomic, index: StructureElement.UnitIndex) {
+    return unit.model.atomicHierarchy.atoms.pdbx_formal_charge.value(unit.elements[index])
+}
+
+export function atomId(unit: Unit.Atomic, index: StructureElement.UnitIndex) {
+    return unit.model.atomicHierarchy.atoms.label_atom_id.value(unit.elements[index])
+}
+
+export function altLoc(unit: Unit.Atomic, index: StructureElement.UnitIndex) {
+    return unit.model.atomicHierarchy.atoms.label_alt_id.value(unit.elements[index])
+}
+
+export function compId(unit: Unit.Atomic, index: StructureElement.UnitIndex) {
+    return unit.model.atomicHierarchy.residues.label_comp_id.value(unit.elements[index])
+}
+
+//
+
+export function interBondCount(structure: Structure, unit: Unit.Atomic, index: StructureElement.UnitIndex): number {
+    return structure.interUnitBonds.getBondIndices(index, unit).length
+}
+
+export function intraBondCount(unit: Unit.Atomic, index: StructureElement.UnitIndex): number {
+    const { offset } = unit.links
+    return offset[index + 1] - offset[index]
+}
+
+export function bondCount(structure: Structure, unit: Unit.Atomic, index: StructureElement.UnitIndex): number {
+    return interBondCount(structure, unit, index) + intraBondCount(unit, index)
+}
+
+export function bondToElementCount(structure: Structure, unit: Unit.Atomic, index: StructureElement.UnitIndex, element: Elements): number {
+    let count = 0
+    eachBondedAtom(structure, unit, index, (unit: Unit.Atomic, index: StructureElement.UnitIndex) => {
+        if (typeSymbol(unit, index) === element) count += 1
+    })
+    return count
+}
+
+//
+
+export function intraConnectedTo(unit: Unit.Atomic, indexA: StructureElement.UnitIndex, indexB: StructureElement.UnitIndex) {
+    const { offset, b } = unit.links
+    for (let i = offset[indexA], il = offset[indexA + 1]; i < il; ++i) {
+        if (b[i] === indexB) return true
+    }
+    return false
+}
+
+//
+
+export function eachInterBondedAtom(structure: Structure, unit: Unit.Atomic, index: StructureElement.UnitIndex, cb: (unit: Unit.Atomic, index: StructureElement.UnitIndex) => void): void {
+    // inter
+    const interIndices = structure.interUnitBonds.getBondIndices(index, unit)
+    for (let i = 0, il = interIndices.length; i < il; ++i) {
+        const b = structure.interUnitBonds.bonds[i]
+        cb(b.unitB, b.indexB)
+    }
+}
+
+export function eachIntraBondedAtom(unit: Unit.Atomic, index: StructureElement.UnitIndex, cb: (unit: Unit.Atomic, index: StructureElement.UnitIndex) => void): void {
+    // intra
+    const { offset, b } = unit.links
+    for (let i = offset[index], il = offset[index + 1]; i < il; ++i) {
+        cb(unit, b[i] as StructureElement.UnitIndex)
+    }
+}
+
+export function eachBondedAtom(structure: Structure, unit: Unit.Atomic, index: StructureElement.UnitIndex, cb: (unit: Unit.Atomic, index: StructureElement.UnitIndex) => void): void {
+    eachInterBondedAtom(structure, unit, index, cb)
+    eachIntraBondedAtom(unit, index, cb)
+}

+ 323 - 0
src/mol-model-props/computed/chemistry/valence-model.ts

@@ -0,0 +1,323 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Fred Ludlow <Fred.Ludlow@astx.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Structure, StructureElement, Unit, Link } from '../../../mol-model/structure';
+import { Elements, isMetal } from '../../../mol-model/structure/model/properties/atomic/types';
+import { AtomGeometry, assignGeometry } from './geometry';
+import { bondCount, typeSymbol, formalCharge, bondToElementCount } from './util';
+import { ParamDefinition as PD } from '../../../mol-util/param-definition';
+import { RuntimeContext } from '../../../mol-task';
+
+/**
+ * TODO:
+ *   Ensure proper treatment of disorder/models. e.g. V257 N in 5vim
+ *   Formal charge of 255 for SO4 anion (e.g. 5ghl)
+ *   Have removed a lot of explicit features (as I think they're more
+ *   generally captured by better VM).
+ *     Could we instead have a "delocalised negative/positive" charge
+ *     feature and flag these up?
+ *
+ */
+
+const tmpConjLinkItA = new Link.ElementLinkIterator()
+const tmpConjLinkItB = new Link.ElementLinkIterator()
+
+/**
+ * Are we involved in some kind of pi system. Either explicitly forming
+ * double bond or N, O next to a double bond, except:
+ *
+ *   N,O with degree 4 cannot be conjugated.
+ *   N,O adjacent to P=O or S=O do not qualify (keeps sulfonamide N sp3 geom)
+ */
+function isConjugated (structure: Structure, unit: Unit.Atomic, index: StructureElement.UnitIndex) {
+    const element = typeSymbol(unit, index)
+    const hetero = element === Elements.O || element === Elements.N
+
+    if (hetero && bondCount(structure, unit, index) === 4) return false
+
+    tmpConjLinkItA.setElement(structure, unit, index)
+    while (tmpConjLinkItA.hasNext) {
+        const bA = tmpConjLinkItA.move()
+        if (bA.order > 1) return true
+        if (hetero) {
+            const elementB = typeSymbol(bA.otherUnit, bA.otherIndex)
+            tmpConjLinkItB.setElement(structure, bA.otherUnit, bA.otherIndex)
+            while (tmpConjLinkItB.hasNext) {
+                const bB = tmpConjLinkItB.move()
+                if (bB.order > 1) {
+                    if ((elementB === Elements.P || elementB === Elements.S) &&
+                            typeSymbol(bB.otherUnit, bB.otherIndex) === Elements.O) {
+                        continue
+                    }
+                    return true
+                }
+            }
+        }
+    }
+
+    return false
+}
+
+export function explicitValence (structure: Structure, unit: Unit.Atomic, index: StructureElement.UnitIndex) {
+    let v = 0
+    // intra-unit bonds
+    const { offset, edgeProps } = unit.links
+    for (let i = offset[index], il = offset[index + 1]; i < il; ++i) v += edgeProps.order[i]
+    // inter-unit bonds
+    structure.interUnitBonds.getBondIndices(index, unit).forEach(b => v += structure.interUnitBonds.bonds[b].order)
+    return v
+}
+
+const tmpChargeLinkItA = new Link.ElementLinkIterator()
+const tmpChargeLinkItB = new Link.ElementLinkIterator()
+
+/**
+ * Attempts to produce a consistent charge and implicit
+ * H-count for an atom.
+ *
+ * If both props.assignCharge and props.assignH, this
+ * approximately follows the rules described in
+ * https://docs.eyesopen.com/toolkits/python/oechemtk/valence.html#openeye-hydrogen-count-model
+ *
+ * If only charge or hydrogens are to be assigned it takes
+ * a much simpler view and deduces one from the other
+ */
+export function calculateHydrogensCharge (structure: Structure, unit: Unit.Atomic, index: StructureElement.UnitIndex, props: ValenceModelProps) {
+    const hydrogenCount = bondToElementCount(structure, unit, index, Elements.H)
+    const element = typeSymbol(unit, index)
+    let charge = formalCharge(unit, index)
+
+    const assignCharge = (props.assignCharge === 'always' || (props.assignCharge === 'auto' && charge === 0))
+    const assignH = (props.assignH === 'always' || (props.assignH === 'auto' && hydrogenCount === 0))
+
+    const degree = bondCount(structure, unit, index)
+    const valence = explicitValence(structure, unit, index)
+
+    const conjugated = isConjugated(structure, unit, index)
+    const multiBond = (valence - degree > 0)
+
+    let implicitHCount = 0
+    let geom = AtomGeometry.Unknown
+
+    switch (element) {
+        case Elements.H:
+            if (assignCharge) {
+                if (degree === 0) {
+                    charge = 1
+                    geom = AtomGeometry.Spherical
+                } else if (degree === 1) {
+                    charge = 0
+                    geom = AtomGeometry.Terminal
+                }
+            }
+            break
+
+        case Elements.C:
+            // TODO: Isocyanide?
+            if (assignCharge) {
+                charge = 0 // Assume carbon always neutral
+            }
+            if (assignH) {
+                // Carbocation/carbanion are 3-valent
+                implicitHCount = Math.max(0, 4 - valence - Math.abs(charge))
+            }
+            // Carbocation is planar, carbanion is tetrahedral
+            geom = assignGeometry(degree + implicitHCount + Math.max(0, -charge))
+            break
+
+        case Elements.N:
+            if (assignCharge) {
+                if (!assignH) { // Trust input H explicitly:
+                    charge = valence - 3
+                } else if (conjugated && valence < 4) {
+                    // Neutral unless amidine/guanidine double-bonded N:
+                    if (degree - hydrogenCount === 1 && valence - hydrogenCount === 2) {
+                        charge = 1
+                    } else {
+                        charge = 0
+                    }
+                } else {
+                    // Sulfonamide nitrogen and classed as sp3 in conjugation model but
+                    // they won't be charged
+                    // Don't assign charge to nitrogens bound to metals
+                    tmpChargeLinkItA.setElement(structure, unit, index)
+                    while (tmpChargeLinkItA.hasNext) {
+                        const b = tmpChargeLinkItA.move()
+                        const elementB = typeSymbol(b.otherUnit, b.otherIndex)
+                        if (elementB === Elements.S || isMetal(elementB)) {
+                            charge = 0
+                            break
+                        } else {
+                            charge = 1
+                        }
+                    }
+                    // TODO: Planarity sanity check?
+                }
+
+            }
+
+            if (assignH) {
+                // NH4+ -> 4, 1' amide -> 2, nitro N/N+ depiction -> 0
+                implicitHCount = Math.max(0, 3 - valence + charge)
+            }
+
+            if (conjugated && !multiBond) {
+                // Amide, anilinic N etc. cannot consider lone-pair for geometry purposes
+                // Anilinic N geometry is depenent on ring electronics, for our purposes we
+                // assume it's trigonal!
+                geom = assignGeometry(degree + implicitHCount - charge)
+            } else {
+                // Everything else, pyridine, amine, nitrile, lp plays normal role:
+                geom = assignGeometry(degree + implicitHCount + 1 - charge)
+            }
+            break
+
+        case Elements.O:
+            if (assignCharge) {
+                if (!assignH) {
+                    charge = valence - 2
+                }
+                if (valence === 1) {
+                    tmpChargeLinkItA.setElement(structure, unit, index)
+                    b1: while (tmpChargeLinkItA.hasNext) {
+                        const bA = tmpChargeLinkItA.move()
+                        tmpChargeLinkItB.setElement(structure, bA.otherUnit, bA.otherIndex)
+                        while (tmpChargeLinkItB.hasNext) {
+                            const bB = tmpChargeLinkItB.move()
+                            if (
+                                !(bB.otherUnit === unit && bB.otherIndex === index) &&
+                                typeSymbol(bB.otherUnit, bB.otherIndex) === Elements.O &&
+                                bB.order === 2
+                            ) {
+                                charge = -1
+                                break b1
+                            }
+                        }
+                    }
+                }
+            }
+            if (assignH) {
+                // ethanol -> 1, carboxylate -> -1
+                implicitHCount = Math.max(0, 2 - valence + charge)
+            }
+            if (conjugated && !multiBond) {
+                // carboxylate OH, phenol OH, one lone-pair taken up with conjugation
+                geom = assignGeometry(degree + implicitHCount - charge + 1)
+            } else {
+                // Carbonyl (trigonal)
+                geom = assignGeometry(degree + implicitHCount - charge + 2)
+            }
+            break
+
+        // Only handles thiols/thiolates/thioether/sulfonium. Sulfoxides and higher
+        // oxidiation states are assumed neutral S (charge carried on O if required)
+        case Elements.S:
+            if (assignCharge) {
+                if (!assignH) {
+                    if (valence <= 3 && bondToElementCount(structure, unit, index, Elements.O) === 0) {
+                        charge = valence - 2 // e.g. explicitly deprotonated thiol
+                    } else {
+                        charge = 0
+                    }
+                }
+            }
+            if (assignH) {
+                if (valence < 2) {
+                    implicitHCount = Math.max(0, 2 - valence + charge)
+                }
+            }
+            if (valence <= 3) {
+                // Thiol, thiolate, tioether -> tetrahedral
+                geom = assignGeometry(degree + implicitHCount - charge + 2)
+            }
+            break
+
+        case Elements.F:
+        case Elements.CL:
+        case Elements.BR:
+        case Elements.I:
+        case Elements.AT:
+            // Never implicitly protonate halides
+            if (assignCharge) {
+                charge = valence - 1
+            }
+            break
+
+        case Elements.LI:
+        case Elements.NA:
+        case Elements.K:
+        case Elements.RB:
+        case Elements.CS:
+        case Elements.FR:
+            if (assignCharge) {
+                charge = 1 - valence
+            }
+            break
+
+        case Elements.BE:
+        case Elements.MG:
+        case Elements.CA:
+        case Elements.SR:
+        case Elements.BA:
+        case Elements.RA:
+            if (assignCharge) {
+                charge = 2 - valence
+            }
+            break
+
+        default:
+            console.warn('Requested charge, protonation for an unhandled element', element)
+    }
+
+    return [ charge, implicitHCount, implicitHCount + hydrogenCount, geom ]
+}
+
+function calcUnitValenceModel(structure: Structure, unit: Unit.Atomic, props: ValenceModelProps) {
+    const n = unit.elements.length
+
+    const charge = new Int8Array(n)
+    const implicitH = new Int8Array(n)
+    const totalH = new Int8Array(n)
+    const idealGeometry = new Int8Array(n)
+
+    for (let i = 0 as StructureElement.UnitIndex; i < n; ++i) {
+        const [ chg, implH, totH, geom ] = calculateHydrogensCharge(structure, unit, i, props)
+        charge[i] = chg
+        implicitH[i] = implH
+        totalH[i] = totH
+        idealGeometry[i] = geom
+    }
+
+    return { charge, implicitH, totalH, idealGeometry }
+}
+
+export interface ValenceModel {
+    charge: Int8Array,
+    implicitH: Int8Array,
+    totalH: Int8Array,
+    idealGeometry: Int8Array
+}
+
+export const ValenceModelParams = {
+    assignCharge: PD.Select('auto', [['always', 'always'], ['auto', 'auto'], ['never', 'never']]),
+    assignH: PD.Select('auto', [['always', 'always'], ['auto', 'auto'], ['never', 'never']]),
+}
+export type ValenceModelParams = typeof ValenceModelParams
+export type ValenceModelProps = PD.Values<ValenceModelParams>
+
+export async function calcValenceModel(ctx: RuntimeContext, structure: Structure, props: Partial<ValenceModelProps>) {
+    const p = { ...PD.getDefaultValues(ValenceModelParams), ...props }
+    const map = new Map<number, ValenceModel>()
+    for (let i = 0, il = structure.units.length; i < il; ++i) {
+        const u = structure.units[i]
+        if (Unit.isAtomic(u)) {
+            const valenceModel = calcUnitValenceModel(structure, u, p)
+            map.set(u.id, valenceModel)
+        }
+    }
+    return map
+}

+ 35 - 0
src/mol-model-props/computed/interactions.ts

@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { CustomPropertyDescriptor, Structure } from '../../mol-model/structure';
+import { RuntimeContext } from '../../mol-task';
+import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { calcInteractions, Interactions, InteractionsParams as _InteractionsParams } from './interactions/interactions';
+import { CustomStructureProperty } from '../common/custom-property-registry';
+
+export const InteractionsParams = {
+    ..._InteractionsParams
+}
+export type InteractionsParams = typeof InteractionsParams
+export type InteractionsProps = PD.Values<InteractionsParams>
+
+export type InteractionsValue = Map<number, Interactions>
+
+export const InteractionsProvider: CustomStructureProperty.Provider<InteractionsParams, InteractionsValue> = CustomStructureProperty.createProvider({
+    label: 'Interactions',
+    descriptor: CustomPropertyDescriptor({
+        isStatic: true,
+        name: 'molstar_computed_interactions',
+        // TODO `cifExport` and `symbol`
+    }),
+    defaultParams: InteractionsParams,
+    getParams: (data: Structure) => InteractionsParams,
+    isApplicable: (data: Structure) => true,
+    compute: async (ctx: RuntimeContext, data: Structure, props: Partial<InteractionsProps>) => {
+        const p = { ...PD.getDefaultValues(InteractionsParams), ...props }
+        return await calcInteractions(ctx, data, p)
+    }
+})

+ 166 - 0
src/mol-model-props/computed/interactions/features.ts

@@ -0,0 +1,166 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { StructureElement } from '../../../mol-model/structure/structure';
+import { ChunkedArray } from '../../../mol-data/util';
+import { GridLookup3D } from '../../../mol-math/geometry';
+import { OrderedSet } from '../../../mol-data/int';
+
+export { Features }
+
+interface Features {
+    /** center x coordinate */
+    readonly x: ArrayLike<number>
+    /** center y coordinate */
+    readonly y: ArrayLike<number>
+    /** center z coordinate */
+    readonly z: ArrayLike<number>
+    /** number of features */
+    readonly count: number
+    readonly types: ArrayLike<FeatureType>
+    readonly groups: ArrayLike<FeatureGroup>
+    readonly offsets: ArrayLike<number>
+    /** elements of this feature, range for feature i is offsets[i] to offsets[i + 1] */
+    readonly members: ArrayLike<StructureElement.UnitIndex>
+    /** lookup3d based on center coordinates */
+    readonly lookup3d: GridLookup3D
+}
+
+namespace Features {
+    /** maps elements to features, range for element i is offsets[i] to offsets[i + 1] */
+    export type ElementsIndex = {
+        readonly indices: ArrayLike<number>
+        readonly offsets: ArrayLike<number>
+    }
+
+    export function createElementsIndex(features: Features, elementsCount: number): ElementsIndex {
+        const offsets = new Int32Array(elementsCount + 1)
+        const bucketFill = new Int32Array(elementsCount)
+        const bucketSizes = new Int32Array(elementsCount)
+        const { members, count, offsets: featureOffsets } = features
+        for (let i = 0; i < count; ++i) ++bucketSizes[members[i]]
+
+        let offset = 0
+        for (let i = 0; i < elementsCount; i++) {
+            offsets[i] = offset
+            offset += bucketSizes[i]
+        }
+        offsets[elementsCount] = offset
+
+        const indices = new Int32Array(offset)
+        for (let i = 0; i < count; ++i) {
+            for (let j = featureOffsets[i], jl = featureOffsets[i + 1]; j < jl; ++j) {
+                const a = members[j]
+                const oa = offsets[a] + bucketFill[a]
+                indices[oa] = i
+                ++bucketFill[a]
+            }
+        }
+
+        return { indices, offsets }
+    }
+}
+
+export { FeaturesBuilder }
+
+interface FeaturesBuilder {
+    clearState: () => void
+    pushMember: (x: number, y: number, z: number, member: StructureElement.UnitIndex) => void
+    addState: (type: FeatureType, group: FeatureGroup) => void
+    addOne: (type: FeatureType, group: FeatureGroup, x: number, y: number, z: number, member: StructureElement.UnitIndex) => void
+    getFeatures: () => Features
+}
+
+namespace FeaturesBuilder {
+    interface State { x: number, y: number, z: number, offset: number, count: number }
+
+    export function create(initialCount = 2048, chunkSize = 1024, features?: Features): FeaturesBuilder {
+        const xCenters = ChunkedArray.create(Float32Array, 1, chunkSize, features ? features.x : initialCount)
+        const yCenters = ChunkedArray.create(Float32Array, 1, chunkSize, features ? features.y : initialCount)
+        const zCenters = ChunkedArray.create(Float32Array, 1, chunkSize, features ? features.z : initialCount)
+        const types = ChunkedArray.create(Uint8Array, 1, chunkSize, features ? features.types : initialCount)
+        const groups = ChunkedArray.create(Uint8Array, 1, chunkSize, features ? features.groups : initialCount)
+        const offsets = ChunkedArray.create(Uint32Array, 1, chunkSize, features ? features.offsets : initialCount)
+        const members = ChunkedArray.create(Uint32Array, 1, chunkSize, features ? features.members : initialCount)
+
+        const state: State = { x: 0, y: 0, z: 0, offset: 0, count: 0 }
+
+        return {
+            clearState: () => {
+                state.x = 0, state.y = 0, state.z = 0, state.offset = members.elementCount, state.count = 0
+            },
+            pushMember: (x: number, y: number, z: number, member: StructureElement.UnitIndex) => {
+                ChunkedArray.add(members, member)
+                state.x += x, state.y += y, state.z += z
+            },
+            addState: (type: FeatureType, group: FeatureGroup) => {
+                const { count } = state
+                if (count === 0) return
+                ChunkedArray.add(types, type)
+                ChunkedArray.add(groups, group)
+                ChunkedArray.add(xCenters, state.x / count)
+                ChunkedArray.add(yCenters, state.y / count)
+                ChunkedArray.add(zCenters, state.z / count)
+                ChunkedArray.add(offsets, state.offset)
+            },
+            addOne: (type: FeatureType, group: FeatureGroup, x: number, y: number, z: number, member: StructureElement.UnitIndex) => {
+                ChunkedArray.add(types, type)
+                ChunkedArray.add(groups, group)
+                ChunkedArray.add(xCenters, x)
+                ChunkedArray.add(yCenters, y)
+                ChunkedArray.add(zCenters, z)
+                ChunkedArray.add(offsets, members.elementCount)
+                ChunkedArray.add(members, member)
+            },
+            getFeatures: () => {
+                ChunkedArray.add(offsets, members.elementCount)
+                const x = ChunkedArray.compact(xCenters, true) as ArrayLike<number>
+                const y = ChunkedArray.compact(yCenters, true) as ArrayLike<number>
+                const z = ChunkedArray.compact(zCenters, true) as ArrayLike<number>
+                const count = xCenters.elementCount
+                return {
+                    x, y, z, count,
+                    types: ChunkedArray.compact(types, true) as ArrayLike<FeatureType>,
+                    groups: ChunkedArray.compact(groups, true) as ArrayLike<FeatureGroup>,
+                    offsets: ChunkedArray.compact(offsets, true) as ArrayLike<number>,
+                    members: ChunkedArray.compact(members, true) as ArrayLike<StructureElement.UnitIndex>,
+                    lookup3d: GridLookup3D({ x, y, z, indices: OrderedSet.ofBounds(0, count) }),
+                }
+            }
+        }
+    }
+}
+
+export const enum FeatureType {
+    None = 0,
+    PositiveCharge = 1,
+    NegativeCharge = 2,
+    AromaticRing = 3,
+    HydrogenDonor = 4,
+    HydrogenAcceptor = 5,
+    HalogenDonor = 6,
+    HalogenAcceptor = 7,
+    Hydrophobic = 8,
+    WeakHydrogenDonor = 9,
+    IonicTypePartner = 10,
+    DativeBondPartner = 11,
+    TransitionMetal = 12,
+    IonicTypeMetal = 13
+}
+
+export const enum FeatureGroup {
+    None = 0,
+    QuaternaryAmine = 1,
+    TertiaryAmine = 2,
+    Sulfonium = 3,
+    SulfonicAcid = 4,
+    Sulfate = 5,
+    Phosphate = 6,
+    Halocarbon = 7,
+    Guanidine = 8,
+    Acetamidine = 9,
+    Carboxylate = 10
+}

+ 287 - 0
src/mol-model-props/computed/interactions/hydrogen-bonds.ts

@@ -0,0 +1,287 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ParamDefinition as PD } from '../../../mol-util/param-definition';
+import { Structure, Unit, StructureElement } from '../../../mol-model/structure';
+import { AtomGeometry, AtomGeometryAngles, calcAngles, calcPlaneAngle } from '../chemistry/geometry';
+import { FeatureType, FeaturesBuilder, FeatureGroup, Features } from './features';
+import { MoleculeType, ProteinBackboneAtoms } from '../../../mol-model/structure/model/types';
+import { typeSymbol, bondToElementCount, bondCount, formalCharge, atomId, compId, intraConnectedTo, altLoc } from '../chemistry/util';
+import { Elements } from '../../../mol-model/structure/model/properties/atomic/types';
+import { InteractionType, InteractionsBuilder } from './interactions';
+import { ValenceModelProvider } from '../valence-model';
+import { degToRad } from '../../../mol-math/misc';
+
+export interface HydrogenBonds {
+
+}
+
+export const HydrogenBondsParams = {
+    maxHbondDist: PD.Numeric(3.5, { min: 1, max: 5, step: 0.1 }),
+    maxHbondSulfurDist: PD.Numeric(4.1, { min: 1, max: 5, step: 0.1 }),
+    maxHbondAccAngleDev: PD.Numeric(45, { min: 0, max: 180, step: 1 }, { description: 'Max deviation from ideal acceptor angle' }),
+    maxHbondDonAngleDev: PD.Numeric(45, { min: 0, max: 180, step: 1 }, { description: 'Max deviation from ideal donor angle' }),
+    maxHbondAccOutOfPlaneAngle: PD.Numeric(90, { min: 0, max: 180, step: 1 }),
+    maxHbondDonOutOfPlaneAngle: PD.Numeric(30, { min: 0, max: 180, step: 1 }),
+}
+export type HydrogenBondsParams = typeof HydrogenBondsParams
+export type HydrogenBondsProps = PD.Values<HydrogenBondsParams>
+
+//
+
+// Geometric characteristics of hydrogen bonds involving sulfur atoms in proteins
+// https://doi.org/10.1002/prot.22327
+
+// Satisfying Hydrogen Bonding Potential in Proteins (HBPLUS)
+// https://doi.org/10.1006/jmbi.1994.1334
+// http://www.csb.yale.edu/userguides/datamanip/hbplus/hbplus_descrip.html
+
+function getUnitValenceModel(structure: Structure, unit: Unit.Atomic) {
+    const valenceModel = ValenceModelProvider.getValue(structure).value
+    if (!valenceModel) throw Error('expected valence model to be available')
+    const unitValenceModel = valenceModel.get(unit.id)
+    if (!unitValenceModel) throw Error('expected valence model for unit to be available')
+    return unitValenceModel
+}
+
+/**
+ * Potential hydrogen donor
+ */
+export function addUnitHydrogenDonors(structure: Structure, unit: Unit.Atomic, builder: FeaturesBuilder) {
+    const { totalH } = getUnitValenceModel(structure, unit)
+    const { elements, conformation } = unit
+    const { x, y, z } = conformation
+
+    for (let i = 0 as StructureElement.UnitIndex, il = elements.length; i < il; ++i) {
+        const element = typeSymbol(unit, i)
+        if ((
+                // include both nitrogen atoms in histidine due to
+                // their often ambiguous protonation assignment
+                isHistidineNitrogen(unit, i)
+            ) || (
+                totalH[i] > 0 &&
+                (element === Elements.N || element === Elements.O || element === Elements.S)
+            )
+        ) {
+            builder.addOne(FeatureType.HydrogenDonor, FeatureGroup.None, x(elements[i]), y(elements[i]), z(elements[i]), i)
+        }
+    }
+}
+
+/**
+ * Weak hydrogen donor.
+ */
+export function addUnitWeakHydrogenDonors(structure: Structure, unit: Unit.Atomic, builder: FeaturesBuilder) {
+    const { totalH } = getUnitValenceModel(structure, unit)
+    const { elements, conformation } = unit
+    const { x, y, z } = conformation
+
+    for (let i = 0 as StructureElement.UnitIndex, il = elements.length; i < il; ++i) {
+        if (
+            typeSymbol(unit, i) === Elements.C &&
+            totalH[i] > 0 &&
+            (
+                bondToElementCount(structure, unit, i, Elements.N) > 0 ||
+                bondToElementCount(structure, unit, i, Elements.O) > 0 ||
+                inAromaticRingWithElectronNegativeElement(structure, unit, i)
+            )
+        ) {
+            builder.addOne(FeatureType.WeakHydrogenDonor, FeatureGroup.None, x(elements[i]), y(elements[i]), z(elements[i]), i)
+        }
+    }
+}
+
+function inAromaticRingWithElectronNegativeElement(structure: Structure, unit: Unit.Atomic, index: StructureElement.UnitIndex) {
+    return false // TODO
+    // if (!a.isAromatic()) return false
+
+    // const ringData = a.residueType.getRings()
+    // if (!ringData) return false
+
+    // let hasElement = false
+    // const rings = ringData.rings
+    // rings.forEach(ring => {
+    //     if (hasElement) return  // already found one
+    //     if (ring.some(idx => (a.index - a.residueAtomOffset) === idx)) {  // in ring
+    //         hasElement = ring.some(idx => {
+    //             const atomTypeId = a.residueType.atomTypeIdList[ idx ]
+    //             const number = a.atomMap.get(atomTypeId).number
+    //             return number === Elements.N || number === Elements.O
+    //         })
+    //     }
+    // })
+
+    // return hasElement
+}
+
+/**
+ * Potential hydrogen acceptor
+ */
+export function addUnitHydrogenAcceptors(structure: Structure, unit: Unit.Atomic, builder: FeaturesBuilder) {
+    const { charge, implicitH, idealGeometry } = getUnitValenceModel(structure, unit)
+    const { elements, conformation } = unit
+    const { x, y, z } = conformation
+
+    function add(i: StructureElement.UnitIndex) {
+        builder.addOne(FeatureType.HydrogenAcceptor, FeatureGroup.None, x(elements[i]), y(elements[i]), z(elements[i]), i)
+    }
+
+    for (let i = 0 as StructureElement.UnitIndex, il = elements.length; i < il; ++i) {
+        const element = typeSymbol(unit, i)
+        if (element === Elements.O) {
+            // Basically assume all oxygen atoms are acceptors!
+            add(i)
+        } else if (element === Elements.N) {
+            if (isHistidineNitrogen(unit, i)) {
+                // include both nitrogen atoms in histidine due to
+                // their often ambiguous protonation assignment
+                add(i)
+            } else if (charge[i] < 1) {
+                // Neutral nitrogen might be an acceptor
+                // It must have at least one lone pair not conjugated
+                const totalBonds = bondCount(structure, unit, i) + implicitH[i]
+                const ig = idealGeometry[i]
+                if (
+                    (ig === AtomGeometry.Tetrahedral && totalBonds < 4) ||
+                    (ig === AtomGeometry.Trigonal && totalBonds < 3) ||
+                    (ig === AtomGeometry.Linear && totalBonds < 2)
+                ) {
+                    add(i)
+                }
+            }
+        } else if (element === Elements.S) {
+            const resname = compId(unit, i)
+            if (resname === 'CYS' || resname === 'MET' || formalCharge(unit, i) === -1) {
+                add(i)
+            }
+        }
+    }
+}
+
+
+function isWater(unit: Unit.Atomic, index: StructureElement.UnitIndex) {
+    return unit.model.atomicHierarchy.derived.residue.moleculeType[unit.elements[index]] === MoleculeType.Water
+}
+
+function isBackbone(unit: Unit.Atomic, index: StructureElement.UnitIndex) {
+    return ProteinBackboneAtoms.has(atomId(unit, index))
+}
+
+function isRing(unit: Unit.Atomic, index: StructureElement.UnitIndex) {
+    return unit.rings.elementRingIndices.has(index)
+}
+
+function isHistidineNitrogen(unit: Unit.Atomic, index: StructureElement.UnitIndex) {
+    return compId(unit, index) === 'HIS' && typeSymbol(unit, index) === Elements.N && isRing(unit, index)
+}
+
+function isBackboneHydrogenBond(unitA: Unit.Atomic, indexA: StructureElement.UnitIndex, unitB: Unit.Atomic, indexB: StructureElement.UnitIndex) {
+    return isBackbone(unitA, indexA) && isBackbone(unitB, indexB)
+}
+
+function isWaterHydrogenBond(unitA: Unit.Atomic, indexA: StructureElement.UnitIndex, unitB: Unit.Atomic, indexB: StructureElement.UnitIndex) {
+    return isWater(unitA, indexA) && isWater(unitB, indexB)
+}
+
+function isHydrogenBond(ti: FeatureType, tj: FeatureType) {
+    return (
+        (ti === FeatureType.HydrogenAcceptor && tj === FeatureType.HydrogenDonor) ||
+        (ti === FeatureType.HydrogenDonor && tj === FeatureType.HydrogenAcceptor)
+    )
+}
+
+function isWeakHydrogenBond(ti: FeatureType, tj: FeatureType) {
+    return (
+        (ti === FeatureType.WeakHydrogenDonor && tj === FeatureType.HydrogenAcceptor) ||
+        (ti === FeatureType.HydrogenAcceptor && tj === FeatureType.WeakHydrogenDonor)
+    )
+}
+
+function getHydrogenBondType(unitA: Unit.Atomic, indexA: StructureElement.UnitIndex, unitB: Unit.Atomic, indexB: StructureElement.UnitIndex) {
+    if (isWaterHydrogenBond(unitA, indexA, unitB, indexB)) {
+        return InteractionType.WaterHydrogenBond
+    } else if (isBackboneHydrogenBond(unitA, indexA, unitB, indexB)) {
+        return InteractionType.BackboneHydrogenBond
+    } else {
+        return InteractionType.HydrogenBond
+    }
+}
+
+/**
+ * All pairs of hydrogen donor and acceptor atoms
+ */
+export function addHydrogenBonds(structure: Structure, unit: Unit.Atomic, features: Features, builder: InteractionsBuilder, props: HydrogenBondsProps) {
+    const { maxHbondDist, maxHbondSulfurDist, maxHbondAccAngleDev, maxHbondDonAngleDev, maxHbondAccOutOfPlaneAngle, maxHbondDonOutOfPlaneAngle } = props
+
+    const maxAccAngleDev = degToRad(maxHbondAccAngleDev)
+    const maxDonAngleDev = degToRad(maxHbondDonAngleDev)
+    const maxAccOutOfPlaneAngle = degToRad(maxHbondAccOutOfPlaneAngle)
+    const maxDonOutOfPlaneAngle = degToRad(maxHbondDonOutOfPlaneAngle)
+
+    const maxDist = Math.max(maxHbondDist, maxHbondSulfurDist)
+    const maxHbondDistSq = maxHbondDist * maxHbondDist
+
+    const { x, y, z, count: n, types, offsets, members, lookup3d } = features
+
+    const valenceModel = ValenceModelProvider.getValue(structure).value
+    if (!valenceModel || !valenceModel.has(unit.id)) throw new Error('valence model required')
+
+    const { idealGeometry } = valenceModel.get(unit.id)!
+
+    for (let i = 0; i < n; ++i) {
+        const { count, indices, squaredDistances } = lookup3d.find(x[i], y[i], z[i], maxDist)
+        const ti = types[i]
+
+        for (let r = 0; r < count; ++r) {
+            const j = indices[r]
+            if (j <= i) continue
+            const dSq = squaredDistances[r]
+            const tj = types[j]
+
+            const isWeak = isWeakHydrogenBond(ti, tj)
+            if (!isWeak && !isHydrogenBond(ti, tj)) continue
+
+            const [ l, k ] = tj === FeatureType.HydrogenAcceptor ? [ i, j ] : [ j, i ]
+
+            const donorIdx = members[offsets[l]]
+            const acceptorIdx = members[offsets[k]]
+
+            if (acceptorIdx === donorIdx) continue // DA to self
+
+            const altD = altLoc(unit, donorIdx)
+            const altA = altLoc(unit, acceptorIdx)
+
+            if (altD && altA && altD !== altA) continue // incompatible alternate location id
+            if (unit.residueIndex[donorIdx] === unit.residueIndex[acceptorIdx]) continue // same residue
+            // check if distance is ok for non-sulfur-containing hbond
+            if (typeSymbol(unit, donorIdx) !== Elements.S && typeSymbol(unit, acceptorIdx) !== Elements.S && dSq > maxHbondDistSq) continue
+            // no hbond if donor and acceptor are bonded
+            if (intraConnectedTo(unit, donorIdx, acceptorIdx)) continue // TODO limit to covalent bonds
+
+            const donAngles = calcAngles(structure, unit, donorIdx, unit, acceptorIdx)
+            const idealDonAngle = AtomGeometryAngles.get(idealGeometry[donorIdx]) || degToRad(120)
+            if (donAngles.some(donAngle => Math.abs(idealDonAngle - donAngle) > maxDonAngleDev)) continue
+
+            if (idealGeometry[donorIdx] === AtomGeometry.Trigonal) {
+                const outOfPlane = calcPlaneAngle(structure, unit, donorIdx, unit, acceptorIdx)
+                if (outOfPlane !== undefined && outOfPlane > maxDonOutOfPlaneAngle) continue
+            }
+
+            const accAngles = calcAngles(structure, unit, acceptorIdx, unit, donorIdx)
+            const idealAccAngle = AtomGeometryAngles.get(idealGeometry[acceptorIdx]) || degToRad(120)
+            // Do not limit large acceptor angles
+            if (accAngles.some(accAngle => idealAccAngle - accAngle > maxAccAngleDev)) continue
+
+            if (idealGeometry[acceptorIdx] === AtomGeometry.Trigonal) {
+                const outOfPlane = calcPlaneAngle(structure, unit, acceptorIdx, unit, donorIdx)
+                if (outOfPlane !== undefined && outOfPlane > maxAccOutOfPlaneAngle) continue
+            }
+
+            const bondType = isWeak ? InteractionType.WeakHydrogenBond : getHydrogenBondType(unit, donorIdx, unit, acceptorIdx)
+            builder.add(l, k, bondType)
+        }
+    }
+}

+ 122 - 0
src/mol-model-props/computed/interactions/interactions.ts

@@ -0,0 +1,122 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ParamDefinition as PD } from '../../../mol-util/param-definition';
+import { Structure, Unit, StructureElement } from '../../../mol-model/structure';
+import { addUnitHydrogenDonors, addUnitWeakHydrogenDonors, addUnitHydrogenAcceptors, addHydrogenBonds, HydrogenBondsParams } from './hydrogen-bonds';
+import { Features, FeaturesBuilder } from './features';
+import { ValenceModelProvider } from '../valence-model';
+import { IntAdjacencyGraph } from '../../../mol-math/graph';
+import { RuntimeContext } from '../../../mol-task';
+
+export const enum InteractionType {
+    Unknown = 0,
+    IonicInteraction = 1,
+    CationPi = 2,
+    PiStacking = 3,
+    HydrogenBond = 4,
+    HalogenBond = 5,
+    Hydrophobic = 6,
+    MetalCoordination = 7,
+    WeakHydrogenBond = 8,
+    WaterHydrogenBond = 9,
+    BackboneHydrogenBond = 10
+}
+
+export { InteractionsBuilder }
+
+interface InteractionsBuilder {
+    add: (indexA: number, indexB: number, type: InteractionType) => void
+    getInteractions: () => Interactions
+}
+
+namespace InteractionsBuilder {
+    export function create(features: Features, elementsCount: number): InteractionsBuilder {
+        const aIndices: number[] = []
+        const bIndices: number[] = []
+        const types: number[] = []
+
+        return {
+            add(indexA: number, indexB: number, type: InteractionType) {
+                aIndices[aIndices.length] = indexA
+                bIndices[bIndices.length] = indexB
+                types[types.length] = type
+            },
+            getInteractions() {
+                const builder = new IntAdjacencyGraph.EdgeBuilder(features.count, aIndices, bIndices)
+                const _types = new Int8Array(builder.slotCount) as ArrayLike<InteractionType>
+                for (let i = 0, _i = builder.edgeCount; i < _i; i++) {
+                    builder.addNextEdge()
+                    builder.assignProperty(_types, types[i])
+                }
+                const links = builder.createGraph({ types: _types })
+
+                const elementsIndex = Features.createElementsIndex(features, elementsCount)
+
+                return {
+                    links,
+                    features,
+                    getLinkIndex: (indexA: StructureElement.UnitIndex, indexB: StructureElement.UnitIndex) => {
+                        // TODO quadratic runtime... when both indices are part of more than one feature
+                        const { indices, offsets } = elementsIndex
+                        for (let i = offsets[indexA], il = offsets[indexA + 1]; i < il; ++i) {
+                            const fA = indices[i]
+                            for (let j = offsets[indexB], jl = offsets[indexB + 1]; j < jl; ++j) {
+                                const fB = indices[j]
+                                let l = links.getDirectedEdgeIndex(fA, fB)
+                                if (l !== -1) return l
+                            }
+                        }
+                        return -1
+                    }
+                }
+            }
+        }
+    }
+}
+
+export type InteractionsLinks = IntAdjacencyGraph<{ readonly types: ArrayLike<InteractionType> }>
+
+export interface Interactions {
+    links: InteractionsLinks
+    features: Features
+    getLinkIndex: (indexA: StructureElement.UnitIndex, indexB: StructureElement.UnitIndex) => number
+}
+
+export const InteractionsParams = {
+    ...HydrogenBondsParams
+}
+export type InteractionsParams = typeof InteractionsParams
+export type InteractionsProps = PD.Values<InteractionsParams>
+
+export async function calcInteractions(runtime: RuntimeContext, structure: Structure, props: Partial<InteractionsProps>) {
+    const p = { ...PD.getDefaultValues(InteractionsParams), ...props }
+    await ValenceModelProvider.attach(structure).runInContext(runtime)
+    const map = new Map<number, Interactions>()
+    for (let i = 0, il = structure.units.length; i < il; ++i) {
+        const u = structure.units[i]
+        if (Unit.isAtomic(u)) {
+            const interactions = calcIntraUnitInteractions(structure, u, p)
+            map.set(u.id, interactions)
+        }
+    }
+    return map
+}
+
+function calcIntraUnitInteractions(structure: Structure, unit: Unit.Atomic, props: InteractionsProps) {
+
+    const featuresBuilder = FeaturesBuilder.create()
+    addUnitHydrogenDonors(structure, unit, featuresBuilder)
+    addUnitWeakHydrogenDonors(structure, unit, featuresBuilder)
+    addUnitHydrogenAcceptors(structure, unit, featuresBuilder)
+
+    const features = featuresBuilder.getFeatures()
+
+    const interactionsBuilder = InteractionsBuilder.create(features, unit.elements.length)
+    addHydrogenBonds(structure, unit, features, interactionsBuilder, props)
+
+    return interactionsBuilder.getInteractions()
+}

+ 35 - 0
src/mol-model-props/computed/valence-model.ts

@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { CustomPropertyDescriptor, Structure } from '../../mol-model/structure';
+import { RuntimeContext } from '../../mol-task';
+import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { calcValenceModel, ValenceModel, ValenceModelParams as _ValenceModelParams } from './chemistry/valence-model';
+import { CustomStructureProperty } from '../common/custom-property-registry';
+
+export const ValenceModelParams = {
+    ..._ValenceModelParams
+}
+export type ValenceModelParams = typeof ValenceModelParams
+export type ValenceModelProps = PD.Values<ValenceModelParams>
+
+export type ValenceModelValue = Map<number, ValenceModel>
+
+export const ValenceModelProvider: CustomStructureProperty.Provider<ValenceModelParams, ValenceModelValue> = CustomStructureProperty.createProvider({
+    label: 'Valence Model',
+    descriptor: CustomPropertyDescriptor({
+        isStatic: true,
+        name: 'molstar_computed_valence_model',
+        // TODO `cifExport` and `symbol`
+    }),
+    defaultParams: ValenceModelParams,
+    getParams: (data: Structure) => ValenceModelParams,
+    isApplicable: (data: Structure) => true,
+    compute: async (ctx: RuntimeContext, data: Structure, props: Partial<ValenceModelProps>) => {
+        const p = { ...PD.getDefaultValues(ValenceModelParams), ...props }
+        return await calcValenceModel(ctx, data, p)
+    }
+})

+ 5 - 4
src/mol-model/structure/model/properties/atomic/measures.ts

@@ -1,21 +1,22 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import { ElementSymbol } from '../../types';
 
-export const AtomicNumbers: { [e: string]: number | undefined } = { 'H': 1, 'D': 1, 'T': 1, 'HE': 2, 'LI': 3, 'BE': 4, 'B': 5, 'C': 6, 'N': 7, 'O': 8, 'F': 9, 'NE': 10, 'NA': 11, 'MG': 12, 'AL': 13, 'SI': 14, 'P': 15, 'S': 16, 'CL': 17, 'AR': 18, 'K': 19, 'CA': 20, 'SC': 21, 'TI': 22, 'V': 23, 'CR': 24, 'MN': 25, 'FE': 26, 'CO': 27, 'NI': 28, 'CU': 29, 'ZN': 30, 'GA': 31, 'GE': 32, 'AS': 33, 'SE': 34, 'BR': 35, 'KR': 36, 'RB': 37, 'SR': 38, 'Y': 39, 'ZR': 40, 'NB': 41, 'MO': 42, 'TC': 43, 'RU': 44, 'RH': 45, 'PD': 46, 'AG': 47, 'CD': 48, 'IN': 49, 'SN': 50, 'SB': 51, 'TE': 52, 'I': 53, 'XE': 54, 'CS': 55, 'BA': 56, 'LA': 57, 'CE': 58, 'PR': 59, 'ND': 60, 'PM': 61, 'SM': 62, 'EU': 63, 'GD': 64, 'TB': 65, 'DY': 66, 'HO': 67, 'ER': 68, 'TM': 69, 'YB': 70, 'LU': 71, 'HF': 72, 'TA': 73, 'W': 74, 'RE': 75, 'OS': 76, 'IR': 77, 'PT': 78, 'AU': 79, 'HG': 80, 'TL': 81, 'PB': 82, 'BI': 83, 'PO': 84, 'AT': 85, 'RN': 86, 'FR': 87, 'RA': 88, 'AC': 89, 'TH': 90, 'PA': 91, 'U': 92, 'NP': 93, 'PU': 94, 'AM': 95, 'CM': 96, 'BK': 97, 'CF': 98, 'ES': 99, 'FM': 100, 'MD': 101, 'NO': 102, 'LR': 103, 'RF': 104, 'DB': 105, 'SG': 106, 'BH': 107, 'HS': 108, 'MT': 109 };
+export const AtomicNumbers: { [e: string]: number | undefined } = {
+    'H': 1, 'D': 1, 'T': 1, 'HE': 2, 'LI': 3, 'BE': 4, 'B': 5, 'C': 6, 'N': 7, 'O': 8, 'F': 9, 'NE': 10, 'NA': 11, 'MG': 12, 'AL': 13, 'SI': 14, 'P': 15, 'S': 16, 'CL': 17, 'AR': 18, 'K': 19, 'CA': 20, 'SC': 21, 'TI': 22, 'V': 23, 'CR': 24, 'MN': 25, 'FE': 26, 'CO': 27, 'NI': 28, 'CU': 29, 'ZN': 30, 'GA': 31, 'GE': 32, 'AS': 33, 'SE': 34, 'BR': 35, 'KR': 36, 'RB': 37, 'SR': 38, 'Y': 39, 'ZR': 40, 'NB': 41, 'MO': 42, 'TC': 43, 'RU': 44, 'RH': 45, 'PD': 46, 'AG': 47, 'CD': 48, 'IN': 49, 'SN': 50, 'SB': 51, 'TE': 52, 'I': 53, 'XE': 54, 'CS': 55, 'BA': 56, 'LA': 57, 'CE': 58, 'PR': 59, 'ND': 60, 'PM': 61, 'SM': 62, 'EU': 63, 'GD': 64, 'TB': 65, 'DY': 66, 'HO': 67, 'ER': 68, 'TM': 69, 'YB': 70, 'LU': 71, 'HF': 72, 'TA': 73, 'W': 74, 'RE': 75, 'OS': 76, 'IR': 77, 'PT': 78, 'AU': 79, 'HG': 80, 'TL': 81, 'PB': 82, 'BI': 83, 'PO': 84, 'AT': 85, 'RN': 86, 'FR': 87, 'RA': 88, 'AC': 89, 'TH': 90, 'PA': 91, 'U': 92, 'NP': 93, 'PU': 94, 'AM': 95, 'CM': 96, 'BK': 97, 'CF': 98, 'ES': 99, 'FM': 100, 'MD': 101, 'NO': 102, 'LR': 103, 'RF': 104, 'DB': 105, 'SG': 106, 'BH': 107, 'HS': 108, 'MT': 109 };
 
 // http://dx.doi.org/10.1021/jp8111556 (or 2.0)
 export const ElementVdwRadii: { [e: number]: number | undefined } = {
-  1: 1.1, 2: 1.4, 3: 1.81, 4: 1.53, 5: 1.92, 6: 1.7, 7: 1.55, 8: 1.52, 9: 1.47, 10: 1.54, 11: 2.27, 12: 1.73, 13: 1.84, 14: 2.1, 15: 1.8, 16: 1.8, 17: 1.75, 18: 1.88, 19: 2.75, 20: 2.31, 21: 2.3, 22: 2.15, 23: 2.05, 24: 2.05, 25: 2.05, 26: 2.05, 27: 2.0, 28: 2.0, 29: 2.0, 30: 2.1, 31: 1.87, 32: 2.11, 33: 1.85, 34: 1.9, 35: 1.83, 36: 2.02, 37: 3.03, 38: 2.49, 39: 2.4, 40: 2.3, 41: 2.15, 42: 2.1, 43: 2.05, 44: 2.05, 45: 2.0, 46: 2.05, 47: 2.1, 48: 2.2, 49: 2.2, 50: 1.93, 51: 2.17, 52: 2.06, 53: 1.98, 54: 2.16, 55: 3.43, 56: 2.68, 57: 2.5, 58: 2.48, 59: 2.47, 60: 2.45, 61: 2.43, 62: 2.42, 63: 2.4, 64: 2.38, 65: 2.37, 66: 2.35, 67: 2.33, 68: 2.32, 69: 2.3, 70: 2.28, 71: 2.27, 72: 2.25, 73: 2.2, 74: 2.1, 75: 2.05, 76: 2.0, 77: 2.0, 78: 2.05, 79: 2.1, 80: 2.05, 81: 1.96, 82: 2.02, 83: 2.07, 84: 1.97, 85: 2.02, 86: 2.2, 87: 3.48, 88: 2.83, 89: 2.0, 90: 2.4, 91: 2.0, 92: 2.3, 93: 2.0, 94: 2.0, 95: 2.0, 96: 2.0, 97: 2.0, 98: 2.0, 99: 2.0, 100: 2.0, 101: 2.0, 102: 2.0, 103: 2.0, 104: 2.0, 105: 2.0, 106: 2.0, 107: 2.0, 108: 2.0, 109: 2.0
+    1: 1.1, 2: 1.4, 3: 1.81, 4: 1.53, 5: 1.92, 6: 1.7, 7: 1.55, 8: 1.52, 9: 1.47, 10: 1.54, 11: 2.27, 12: 1.73, 13: 1.84, 14: 2.1, 15: 1.8, 16: 1.8, 17: 1.75, 18: 1.88, 19: 2.75, 20: 2.31, 21: 2.3, 22: 2.15, 23: 2.05, 24: 2.05, 25: 2.05, 26: 2.05, 27: 2.0, 28: 2.0, 29: 2.0, 30: 2.1, 31: 1.87, 32: 2.11, 33: 1.85, 34: 1.9, 35: 1.83, 36: 2.02, 37: 3.03, 38: 2.49, 39: 2.4, 40: 2.3, 41: 2.15, 42: 2.1, 43: 2.05, 44: 2.05, 45: 2.0, 46: 2.05, 47: 2.1, 48: 2.2, 49: 2.2, 50: 1.93, 51: 2.17, 52: 2.06, 53: 1.98, 54: 2.16, 55: 3.43, 56: 2.68, 57: 2.5, 58: 2.48, 59: 2.47, 60: 2.45, 61: 2.43, 62: 2.42, 63: 2.4, 64: 2.38, 65: 2.37, 66: 2.35, 67: 2.33, 68: 2.32, 69: 2.3, 70: 2.28, 71: 2.27, 72: 2.25, 73: 2.2, 74: 2.1, 75: 2.05, 76: 2.0, 77: 2.0, 78: 2.05, 79: 2.1, 80: 2.05, 81: 1.96, 82: 2.02, 83: 2.07, 84: 1.97, 85: 2.02, 86: 2.2, 87: 3.48, 88: 2.83, 89: 2.0, 90: 2.4, 91: 2.0, 92: 2.3, 93: 2.0, 94: 2.0, 95: 2.0, 96: 2.0, 97: 2.0, 98: 2.0, 99: 2.0, 100: 2.0, 101: 2.0, 102: 2.0, 103: 2.0, 104: 2.0, 105: 2.0, 106: 2.0, 107: 2.0, 108: 2.0, 109: 2.0
 }
 
 // https://doi.org/10.1515/pac-2015-0305 (table 2, 3, and 4)
 export const ElementAtomWeights: { [e: number]: number | undefined } = {
-  1: 1.008, 2: 4.0026, 3: 6.94, 4: 9.0122, 5: 10.81, 6: 10.81, 7: 14.007, 8: 15.999, 9: 18.998, 10: 20.180, 11: 22.990, 12: 24.305, 13: 26.982, 14: 28.085, 15: 30.974, 16: 32.06, 17: 35.45, 18: 39.948, 19: 39.098, 20: 40.078, 21: 44.956, 22: 47.867, 23: 50.942, 24: 51.996, 25: 54.938, 26: 55.845, 27: 58.933, 28: 58.693, 29: 63.546, 30: 65.38, 31: 69.723, 32: 72.630, 33: 74.922, 34: 78.971, 35: 79.904, 36: 83.798, 37: 85.468, 38: 87.62, 39: 88.906, 40: 91.224, 41: 92.906, 42: 95.95, 43: 96.906, 44: 101.07, 45: 102.91, 46: 106.42, 47: 107.87, 48: 112.41, 49: 114.82, 50: 118.71, 51: 121.76, 52: 127.60, 53: 127.60, 54: 131.29, 55: 132.91, 56: 137.33, 57: 138.91, 58: 140.12, 59: 140.91, 60: 144.24, 61: 144.912, 62: 150.36, 63: 151.96, 64: 157.25, 65: 158.93, 66: 162.50, 67: 164.93, 68: 167.26, 69: 168.93, 70: 173.05, 71: 174.97, 72: 178.49, 73: 180.95, 74: 183.84, 75: 186.21, 76: 190.23, 77: 192.22, 78: 195.08, 79: 196.97, 80: 200.59, 81: 204.38, 82: 207.2, 83: 208.98, 84: 1.97, 85: 2.02, 86: 2.2, 87: 3.48, 88: 2.83, 89: 2.0, 90: 232.04, 91: 231.04, 92: 238.03, 93: 237.048, 94: 244.064, 95: 243.061, 96: 247.070, 97: 247.070, 98: 251.079, 99: 252.083, 100: 257.095, 101: 258.098, 102: 259.101, 103: 262.110, 104: 267.122, 105: 270.131, 106: 271.134, 107: 270.133, 108: 270.134, 109: 278.156
+    1: 1.008, 2: 4.0026, 3: 6.94, 4: 9.0122, 5: 10.81, 6: 10.81, 7: 14.007, 8: 15.999, 9: 18.998, 10: 20.180, 11: 22.990, 12: 24.305, 13: 26.982, 14: 28.085, 15: 30.974, 16: 32.06, 17: 35.45, 18: 39.948, 19: 39.098, 20: 40.078, 21: 44.956, 22: 47.867, 23: 50.942, 24: 51.996, 25: 54.938, 26: 55.845, 27: 58.933, 28: 58.693, 29: 63.546, 30: 65.38, 31: 69.723, 32: 72.630, 33: 74.922, 34: 78.971, 35: 79.904, 36: 83.798, 37: 85.468, 38: 87.62, 39: 88.906, 40: 91.224, 41: 92.906, 42: 95.95, 43: 96.906, 44: 101.07, 45: 102.91, 46: 106.42, 47: 107.87, 48: 112.41, 49: 114.82, 50: 118.71, 51: 121.76, 52: 127.60, 53: 127.60, 54: 131.29, 55: 132.91, 56: 137.33, 57: 138.91, 58: 140.12, 59: 140.91, 60: 144.24, 61: 144.912, 62: 150.36, 63: 151.96, 64: 157.25, 65: 158.93, 66: 162.50, 67: 164.93, 68: 167.26, 69: 168.93, 70: 173.05, 71: 174.97, 72: 178.49, 73: 180.95, 74: 183.84, 75: 186.21, 76: 190.23, 77: 192.22, 78: 195.08, 79: 196.97, 80: 200.59, 81: 204.38, 82: 207.2, 83: 208.98, 84: 1.97, 85: 2.02, 86: 2.2, 87: 3.48, 88: 2.83, 89: 2.0, 90: 232.04, 91: 231.04, 92: 238.03, 93: 237.048, 94: 244.064, 95: 243.061, 96: 247.070, 97: 247.070, 98: 251.079, 99: 252.083, 100: 257.095, 101: 258.098, 102: 259.101, 103: 262.110, 104: 267.122, 105: 270.131, 106: 271.134, 107: 270.133, 108: 270.134, 109: 278.156
 }
 
 export const DefaultVdwRadius = 1.7;  // C

+ 78 - 0
src/mol-model/structure/model/properties/atomic/types.ts

@@ -0,0 +1,78 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ElementSymbol } from '../../types';
+import { AtomNumber } from './measures';
+
+/**
+ * Enum of element symbols
+ */
+export const enum Elements {
+    H = 'H', D = 'D', T = 'T', HE = 'HE', LI = 'LI', BE = 'BE', B = 'B', C = 'C', N = 'N', O = 'O', F = 'F', NE = 'NE', NA = 'NA', MG = 'MG', AL = 'AL', SI = 'SI', P = 'P', S = 'S', CL = 'CL', AR = 'AR', K = 'K', CA = 'CA', SC = 'SC', TI = 'TI', V = 'V', CR = 'CR', MN = 'MN', FE = 'FE', CO = 'CO', NI = 'NI', CU = 'CU', ZN = 'ZN', GA = 'GA', GE = 'GE', AS = 'AS', SE = 'SE', BR = 'BR', KR = 'KR', RB = 'RB', SR = 'SR', Y = 'Y', ZR = 'ZR', NB = 'NB', MO = 'MO', TC = 'TC', RU = 'RU', RH = 'RH', PD = 'PD', AG = 'AG', CD = 'CD', IN = 'IN', SN = 'SN', SB = 'SB', TE = 'TE', I = 'I', XE = 'XE', CS = 'CS', BA = 'BA', LA = 'LA', CE = 'CE', PR = 'PR', ND = 'ND', PM = 'PM', SM = 'SM', EU = 'EU', GD = 'GD', TB = 'TB', DY = 'DY', HO = 'HO', ER = 'ER', TM = 'TM', YB = 'YB', LU = 'LU', HF = 'HF', TA = 'TA', W = 'W', RE = 'RE', OS = 'OS', IR = 'IR', PT = 'PT', AU = 'AU', HG = 'HG', TL = 'TL', PB = 'PB', BI = 'BI', PO = 'PO', AT = 'AT', RN = 'RN', FR = 'FR', RA = 'RA', AC = 'AC', TH = 'TH', PA = 'PA', U = 'U', NP = 'NP', PU = 'PU', AM = 'AM', CM = 'CM', BK = 'BK', CF = 'CF', ES = 'ES', FM = 'FM', MD = 'MD', NO = 'NO', LR = 'LR', RF = 'RF', DB = 'DB', SG = 'SG', BH = 'BH', HS = 'HS', MT = 'MT', DS = 'DS', RG = 'RG', CN = 'CN', NH = 'NH', FL = 'FL', MC = 'MC', LV = 'LV', TS = 'TS', OG = 'OG'
+}
+
+export const AlkaliMetals = new Set<ElementSymbol>(['LI', 'NA', 'K', 'RB', 'CS', 'FR'] as ElementSymbol[])
+export function isAlkaliMetal(element: ElementSymbol) { return AlkaliMetals.has(element) }
+
+export const AlkalineEarthMetals = new Set<ElementSymbol>(['BE', 'MG', 'CA', 'SR', 'BA', 'RA'] as ElementSymbol[])
+export function isAlkalineEarthMetal(element: ElementSymbol) { return AlkalineEarthMetals.has(element) }
+
+export const PolyatomicNonmetals = new Set<ElementSymbol>(['C', 'P', 'S', 'SE'] as ElementSymbol[])
+export function isPolyatomicNonmetal(element: ElementSymbol) { return PolyatomicNonmetals.has(element) }
+
+export const DiatomicNonmetals = new Set<ElementSymbol>(['H', 'N', 'O', 'F', 'CL', 'BR', 'I'] as ElementSymbol[])
+export function isDiatomicNonmetal(element: ElementSymbol) { return DiatomicNonmetals.has(element) }
+
+export const NobleGases = new Set<ElementSymbol>(['HE', 'NE', 'AR', 'KR', 'XE', 'RN'] as ElementSymbol[])
+export function isNobleGas(element: ElementSymbol) { return NobleGases.has(element) }
+
+export const PostTransitionMetals = new Set<ElementSymbol>(['ZN', 'GA', 'CD', 'IN', 'SN', 'HG', 'TI', 'PB', 'BI', 'PO', 'CN'] as ElementSymbol[])
+export function isPostTransitionMetal(element: ElementSymbol) { return PostTransitionMetals.has(element) }
+
+export const Metalloids = new Set<ElementSymbol>(['B', 'SI', 'GE', 'AS', 'SB', 'TE', 'AT'] as ElementSymbol[])
+export function isMetalloid(element: ElementSymbol) { return Metalloids.has(element) }
+
+export const Halogens = new Set<ElementSymbol>(['F', 'CL', 'BR', 'I', 'AT'] as ElementSymbol[])
+export function isHalogen(element: ElementSymbol) { return Halogens.has(element) }
+
+export function isTransitionMetal(element: ElementSymbol) {
+    const no = AtomNumber(element)
+    return (
+        (no >= 21 && no <= 29) ||
+        (no >= 39 && no <= 47) ||
+        (no >= 72 && no <= 79) ||
+        (no >= 104 && no <= 108)
+    )
+}
+
+export function isLanthanide (element: ElementSymbol) {
+    const no = AtomNumber(element)
+    return no >= 57 && no <= 71
+}
+
+export function isActinide (element: ElementSymbol) {
+    const no = AtomNumber(element)
+    return no >= 89 && no <= 103
+}
+
+export function isMetal(element: ElementSymbol) {
+    return (
+        isAlkaliMetal(element) ||
+        isAlkalineEarthMetal(element) ||
+        isLanthanide(element) ||
+        isActinide(element) ||
+        isTransitionMetal(element) ||
+        isPostTransitionMetal(element)
+    )
+}
+
+export function isNonmetal(element: ElementSymbol) {
+    return (
+        isDiatomicNonmetal(element) ||
+        isPolyatomicNonmetal(element) ||
+        isNobleGas(element)
+    )
+}

+ 2 - 0
src/mol-plugin/behavior/dynamic/custom-props.ts

@@ -6,6 +6,8 @@
  */
 
 export { AccessibleSurfaceArea } from './custom-props/computed/accessible-surface-area'
+export { Interactions } from './custom-props/computed/interactions'
 export { SecondaryStructure } from './custom-props/computed/secondary-structure'
+
 export { PDBeStructureQualityReport } from './custom-props/pdbe/structure-quality-report'
 export { RCSBAssemblySymmetry } from './custom-props/rcsb/assembly-symmetry'

+ 38 - 0
src/mol-plugin/behavior/dynamic/custom-props/computed/interactions.ts

@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { PluginBehavior } from '../../../behavior';
+import { ParamDefinition as PD } from '../../../../../mol-util/param-definition';
+import { InteractionsProvider } from '../../../../../mol-model-props/computed/interactions';
+
+export const Interactions = PluginBehavior.create<{ autoAttach: boolean }>({
+    name: 'computed-interactions-prop',
+    category: 'custom-props',
+    display: { name: 'Interactions' },
+    ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean }> {
+        private provider = InteractionsProvider
+
+        update(p: { autoAttach: boolean, showTooltip: boolean }) {
+            let updated = (
+                this.params.autoAttach !== p.autoAttach
+            )
+            this.params.autoAttach = p.autoAttach;
+            this.ctx.customStructureProperties.setDefaultAutoAttach(this.provider.descriptor.name, this.params.autoAttach);
+            return updated;
+        }
+
+        register(): void {
+            this.ctx.customStructureProperties.register(this.provider, this.params.autoAttach);
+        }
+
+        unregister() {
+            this.ctx.customStructureProperties.unregister(this.provider.descriptor.name);
+        }
+    },
+    params: () => ({
+        autoAttach: PD.Boolean(false)
+    })
+});

+ 1 - 0
src/mol-plugin/index.ts

@@ -69,6 +69,7 @@ export const DefaultPluginSpec: PluginSpec = {
         PluginSpec.Behavior(PluginBehaviors.Representation.DefaultLociLabelProvider),
         PluginSpec.Behavior(PluginBehaviors.Camera.FocusLoci),
         PluginSpec.Behavior(PluginBehaviors.CustomProps.AccessibleSurfaceArea),
+        PluginSpec.Behavior(PluginBehaviors.CustomProps.Interactions),
         PluginSpec.Behavior(PluginBehaviors.CustomProps.SecondaryStructure),
         PluginSpec.Behavior(PluginBehaviors.CustomProps.PDBeStructureQualityReport, { autoAttach: true, showTooltip: true }),
         PluginSpec.Behavior(PluginBehaviors.CustomProps.RCSBAssemblySymmetry, { autoAttach: true }),

+ 2 - 0
src/mol-repr/structure/registry.ts

@@ -19,6 +19,7 @@ import { MolecularSurfaceRepresentationProvider } from './representation/molecul
 import { EllipsoidRepresentationProvider } from './representation/ellipsoid';
 import { OrientationRepresentationProvider } from './representation/orientation';
 import { LabelRepresentationProvider } from './representation/label';
+import { InteractionsRepresentationProvider } from './representation/interactions';
 
 export class StructureRepresentationRegistry extends RepresentationRegistry<Structure, StructureRepresentationState> {
     constructor() {
@@ -38,6 +39,7 @@ export const BuiltInStructureRepresentations = {
     'ellipsoid': EllipsoidRepresentationProvider,
     'gaussian-surface': GaussianSurfaceRepresentationProvider,
     // 'gaussian-volume': GaussianVolumeRepresentationProvider, // TODO disabled for now, needs more work
+    'interactions': InteractionsRepresentationProvider,
     'label': LabelRepresentationProvider,
     'molecular-surface': MolecularSurfaceRepresentationProvider,
     'orientation': OrientationRepresentationProvider,

+ 49 - 0
src/mol-repr/structure/representation/interactions.ts

@@ -0,0 +1,49 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ParamDefinition as PD } from '../../../mol-util/param-definition';
+import { Representation, RepresentationParamsGetter, RepresentationContext } from '../../representation';
+import { ThemeRegistryContext } from '../../../mol-theme/theme';
+import { Structure } from '../../../mol-model/structure';
+import { UnitsRepresentation, StructureRepresentation, StructureRepresentationStateBuilder, StructureRepresentationProvider } from '../representation';
+import { InteractionsIntraUnitParams, InteractionsIntraUnitVisual } from '../visual/interactions-intra-unit-cylinder';
+import { UnitKindOptions, UnitKind } from '../visual/util/common';
+import { InteractionsProvider } from '../../../mol-model-props/computed/interactions';
+
+const InteractionsVisuals = {
+    'intra-unit': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, InteractionsIntraUnitParams>) => UnitsRepresentation('Intra-unit interactions cylinder', ctx, getParams, InteractionsIntraUnitVisual),
+}
+
+export const InteractionsParams = {
+    ...InteractionsIntraUnitParams,
+    unitKinds: PD.MultiSelect<UnitKind>(['atomic'], UnitKindOptions),
+    sizeFactor: PD.Numeric(0.3, { min: 0.01, max: 10, step: 0.01 }),
+    sizeAspectRatio: PD.Numeric(2/3, { min: 0.01, max: 3, step: 0.01 }),
+    visuals: PD.MultiSelect(['intra-unit'], PD.objectToOptions(InteractionsVisuals)),
+}
+export type InteractionsParams = typeof InteractionsParams
+export function getInteractionParams(ctx: ThemeRegistryContext, structure: Structure) {
+    return PD.clone(InteractionsParams)
+}
+
+export type InteractionRepresentation = StructureRepresentation<InteractionsParams>
+export function InteractionRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, InteractionsParams>): InteractionRepresentation {
+    return Representation.createMulti('Interactions', ctx, getParams, StructureRepresentationStateBuilder, InteractionsVisuals as unknown as Representation.Def<Structure, InteractionsParams>)
+}
+
+export const InteractionsRepresentationProvider: StructureRepresentationProvider<InteractionsParams> = {
+    label: 'Non-covalent Interactions',
+    description: 'Displays non-covalent interactions as dashed cylinders.',
+    factory: InteractionRepresentation,
+    getParams: getInteractionParams,
+    defaultValues: PD.getDefaultValues(InteractionsParams),
+    defaultColorTheme: 'interaction-type',
+    defaultSizeTheme: 'uniform',
+    isApplicable: (structure: Structure) => structure.elementCount > 0,
+    ensureDependencies: (structure: Structure) => {
+        return InteractionsProvider.attach(structure.root)
+    }
+}

+ 154 - 0
src/mol-repr/structure/visual/interactions-intra-unit-cylinder.ts

@@ -0,0 +1,154 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Unit, Link, Structure } from '../../../mol-model/structure';
+import { Vec3 } from '../../../mol-math/linear-algebra';
+import { Loci, EmptyLoci } from '../../../mol-model/loci';
+import { Interval, SortedArray } from '../../../mol-data/int';
+import { ParamDefinition as PD } from '../../../mol-util/param-definition';
+import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
+import { PickingId } from '../../../mol-geo/geometry/picking';
+import { VisualContext } from '../../visual';
+import { Theme } from '../../../mol-theme/theme';
+import { LinkType } from '../../../mol-model/structure/model/types';
+import { InteractionsProvider } from '../../../mol-model-props/computed/interactions';
+import { createLinkCylinderMesh, LinkCylinderParams } from './util/link';
+import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual, StructureGroup } from '../units-visual';
+import { VisualUpdateState } from '../../util';
+import { LocationIterator } from '../../../mol-geo/util/location-iterator';
+
+async function createIntraUnitInteractionsCylinderMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PD.Values<InteractionsIntraUnitParams>, mesh?: Mesh) {
+    if (!Unit.isAtomic(unit)) return Mesh.createEmpty(mesh)
+
+    const interactions = InteractionsProvider.getValue(structure).value!
+    const { features, links } = interactions.get(unit.id)!
+
+    const { x, y, z, offsets, members } = features
+    const { edgeCount, a, b } = links
+    const { sizeFactor } = props
+    const { elements } = unit
+    const rootElements = structure.root.unitMap.get(unit.id).elements
+
+    if (!edgeCount) return Mesh.createEmpty(mesh)
+
+    const builderProps = {
+        linkCount: edgeCount * 2,
+        referencePosition: () => null,
+        position: (posA: Vec3, posB: Vec3, edgeIndex: number) => {
+            Vec3.set(posA, x[a[edgeIndex]], y[a[edgeIndex]], z[a[edgeIndex]])
+            Vec3.set(posB, x[b[edgeIndex]], y[b[edgeIndex]], z[b[edgeIndex]])
+        },
+        order: (edgeIndex: number) => 1,
+        flags: (edgeIndex: number) => LinkType.Flag.MetallicCoordination, // TODO
+        radius: (edgeIndex: number) => sizeFactor,
+        ignore: elements !== rootElements ? (edgeIndex: number) => {
+            for (let i = offsets[a[edgeIndex]], il = offsets[a[edgeIndex] + 1]; i < il; ++i) {
+                if (!SortedArray.has(elements, rootElements[members[i]])) return true
+            }
+            for (let i = offsets[b[edgeIndex]], il = offsets[b[edgeIndex] + 1]; i < il; ++i) {
+                if (!SortedArray.has(elements, rootElements[members[i]])) return true
+            }
+            return false
+        } : () => false
+    }
+
+    return createLinkCylinderMesh(ctx, builderProps, props, mesh)
+}
+
+export const InteractionsIntraUnitParams = {
+    ...UnitsMeshParams,
+    ...LinkCylinderParams,
+    sizeFactor: PD.Numeric(0.3, { min: 0, max: 10, step: 0.01 }),
+}
+export type InteractionsIntraUnitParams = typeof InteractionsIntraUnitParams
+
+export function InteractionsIntraUnitVisual(materialId: number): UnitsVisual<InteractionsIntraUnitParams> {
+    return UnitsMeshVisual<InteractionsIntraUnitParams>({
+        defaultProps: PD.getDefaultValues(InteractionsIntraUnitParams),
+        createGeometry: createIntraUnitInteractionsCylinderMesh,
+        createLocationIterator: createInteractionsIterator,
+        getLoci: getLinkLoci,
+        eachLocation: eachInteraction,
+        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<InteractionsIntraUnitParams>, currentProps: PD.Values<InteractionsIntraUnitParams>) => {
+            state.createGeometry = (
+                newProps.sizeFactor !== currentProps.sizeFactor ||
+                newProps.radialSegments !== currentProps.radialSegments ||
+                newProps.linkScale !== currentProps.linkScale ||
+                newProps.linkSpacing !== currentProps.linkSpacing
+            )
+        }
+    }, materialId)
+}
+
+function getLinkLoci(pickingId: PickingId, structureGroup: StructureGroup, id: number) {
+    const { objectId, instanceId, groupId } = pickingId
+    if (id === objectId) {
+        const { structure, group } = structureGroup
+        const unit = group.units[instanceId]
+        if (Unit.isAtomic(unit)) {
+            const interactions = InteractionsProvider.getValue(structure).value!
+            const { features, links } = interactions.get(unit.id)!
+            const { members, offsets } = features
+            // TODO this uses the first member elements of the features of an interaction as a representative
+            return Link.Loci(structure, [
+                Link.Location(
+                    unit, members[offsets[links.a[groupId]]],
+                    unit, members[offsets[links.b[groupId]]]
+                ),
+                Link.Location(
+                    unit, members[offsets[links.b[groupId]]],
+                    unit, members[offsets[links.a[groupId]]]
+                )
+            ])
+        }
+    }
+    return EmptyLoci
+}
+
+function eachInteraction(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) {
+    let changed = false
+    if (Link.isLoci(loci)) {
+        const { structure, group } = structureGroup
+        if (!Structure.areEquivalent(loci.structure, structure)) return false
+        const unit = group.units[0]
+        if (!Unit.isAtomic(unit)) return false
+        const interactions = InteractionsProvider.getValue(structure).value!
+        const { links, getLinkIndex } = interactions.get(unit.id)!
+        const groupCount = links.edgeCount * 2
+        for (const b of loci.links) {
+            const unitIdx = group.unitIndexMap.get(b.aUnit.id)
+            if (unitIdx !== undefined) {
+                const idx = getLinkIndex(b.aIndex, b.bIndex)
+                if (idx !== -1) {
+                    if (apply(Interval.ofSingleton(unitIdx * groupCount + idx))) changed = true
+                }
+            }
+        }
+    }
+    return changed
+}
+
+function createInteractionsIterator(structureGroup: StructureGroup): LocationIterator {
+    const { structure, group } = structureGroup
+    const unit = group.units[0]
+    const interactions = InteractionsProvider.getValue(structure).value!
+    const { links, features } = interactions.get(unit.id)!
+    const { members, offsets } = features
+    const groupCount = links.edgeCount * 2
+    const instanceCount = group.units.length
+    const location = Link.Location()
+    const getLocation = (groupIndex: number, instanceIndex: number) => {
+        const fA = links.a[groupIndex]
+        const fB = links.b[groupIndex]
+        const instanceUnit = group.units[instanceIndex]
+        location.aUnit = instanceUnit
+        location.aIndex = members[offsets[fA]]
+        location.bUnit = instanceUnit
+        location.bIndex = members[offsets[fB]]
+        return location
+    }
+    return LocationIterator(groupCount, instanceCount, getLocation)
+}

+ 2 - 0
src/mol-theme/color.ts

@@ -33,6 +33,7 @@ import { ModelIndexColorThemeProvider } from './color/model-index';
 import { OccupancyColorThemeProvider } from './color/occupancy';
 import { OperatorNameColorThemeProvider } from './color/operator-name';
 import { OperatorHklColorThemeProvider } from './color/operator-hkl';
+import { InteractionTypeColorThemeProvider } from './color/interaction-type';
 import { AccessibleSurfaceAreaColorThemeProvider } from './color/accessible-surface-area';
 
 export type LocationColor = (location: Location, isSecondary: boolean) => Color
@@ -84,6 +85,7 @@ export const BuiltInColorThemes = {
     'entity-source': EntitySourceColorThemeProvider,
     'hydrophobicity': HydrophobicityColorThemeProvider,
     'illustrative': IllustrativeColorThemeProvider,
+    'interaction-type': InteractionTypeColorThemeProvider,
     'model-index': ModelIndexColorThemeProvider,
     'molecule-type': MoleculeTypeColorThemeProvider,
     'occupancy': OccupancyColorThemeProvider,

+ 119 - 0
src/mol-theme/color/interaction-type.ts

@@ -0,0 +1,119 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Link } from '../../mol-model/structure';
+import { Location } from '../../mol-model/location';
+import { Color, ColorMap } from '../../mol-util/color';
+import { ParamDefinition as PD } from '../../mol-util/param-definition'
+import { InteractionsProvider } from '../../mol-model-props/computed/interactions';
+import { ThemeDataContext } from '../theme';
+import { ColorTheme, LocationColor } from '../color';
+import { InteractionType } from '../../mol-model-props/computed/interactions/interactions';
+import { TableLegend } from '../../mol-util/legend';
+import { Task } from '../../mol-task';
+
+const DefaultColor = Color(0xCCCCCC)
+const Description = 'Assigns colors according the interaction type of a link.'
+
+const InteractionTypeColors = ColorMap({
+    HydrogenBond: 0x2B83BA,
+    Hydrophobic: 0x808080,
+    HalogenBond: 0x40FFBF,
+    Ionic: 0xF0C814,
+    MetalCoordination: 0x8C4099,
+    CationPi: 0xFF8000,
+    PiStacking: 0x8CB366,
+    WeakHydrogenBond: 0xC5DDEC,
+})
+
+const InteractionTypeColorTable: [string, Color][] = [
+    ['Hydrogen Bond', InteractionTypeColors.HydrogenBond],
+    ['Hydrophobic', InteractionTypeColors.Hydrophobic],
+    ['Halogen Bond', InteractionTypeColors.HalogenBond],
+    ['Ionic', InteractionTypeColors.Ionic],
+    ['Metal Coordination', InteractionTypeColors.MetalCoordination],
+    ['Cation Pi', InteractionTypeColors.CationPi],
+    ['Pi Stacking', InteractionTypeColors.PiStacking],
+    ['Weak HydrogenBond', InteractionTypeColors.WeakHydrogenBond],
+]
+
+function typeColor(type: InteractionType): Color {
+    switch (type) {
+        case InteractionType.HydrogenBond:
+        case InteractionType.WaterHydrogenBond:
+        case InteractionType.BackboneHydrogenBond:
+            return InteractionTypeColors.HydrogenBond
+        case InteractionType.Hydrophobic:
+            return InteractionTypeColors.Hydrophobic
+        case InteractionType.HalogenBond:
+            return InteractionTypeColors.HalogenBond
+        case InteractionType.IonicInteraction:
+            return InteractionTypeColors.Ionic
+        case InteractionType.MetalCoordination:
+            return InteractionTypeColors.MetalCoordination
+        case InteractionType.CationPi:
+            return InteractionTypeColors.CationPi
+        case InteractionType.PiStacking:
+            return InteractionTypeColors.PiStacking
+        case InteractionType.WeakHydrogenBond:
+            return InteractionTypeColors.WeakHydrogenBond
+        case InteractionType.Unknown:
+            return DefaultColor
+    }
+}
+
+export const InteractionTypeColorThemeParams = { }
+export type InteractionTypeColorThemeParams = typeof InteractionTypeColorThemeParams
+export function getInteractionTypeColorThemeParams(ctx: ThemeDataContext) {
+    return InteractionTypeColorThemeParams // TODO return copy
+}
+
+export function InteractionTypeColorTheme(ctx: ThemeDataContext, props: PD.Values<InteractionTypeColorThemeParams>): ColorTheme<InteractionTypeColorThemeParams> {
+    let color: LocationColor
+
+    const interactions = ctx.structure ? InteractionsProvider.getValue(ctx.structure) : undefined
+    const contextHash = interactions?.version
+
+    if (interactions && interactions.value) {
+        const map = interactions.value
+        color = (location: Location) => {
+            if (Link.isLocation(location)) {
+                const unitInteractions = map.get(location.aUnit.id)
+                if (unitInteractions) {
+                    const { links, getLinkIndex } = unitInteractions
+                    if (links.edgeCount > 0) {
+                        const idx = getLinkIndex(location.aIndex, location.bIndex)
+                        if (idx !== -1) return typeColor(links.edgeProps.types[idx])
+                    }
+                }
+            }
+            return DefaultColor
+        }
+    } else {
+        color = () => DefaultColor
+    }
+
+    return {
+        factory: InteractionTypeColorTheme,
+        granularity: 'group',
+        color: color,
+        props: props,
+        contextHash,
+        description: Description,
+        legend: TableLegend(InteractionTypeColorTable)
+    }
+}
+
+export const InteractionTypeColorThemeProvider: ColorTheme.Provider<InteractionTypeColorThemeParams> = {
+    label: 'Interaction Type',
+    factory: InteractionTypeColorTheme,
+    getParams: getInteractionTypeColorThemeParams,
+    defaultValues: PD.getDefaultValues(InteractionTypeColorThemeParams),
+    isApplicable: (ctx: ThemeDataContext) => !!ctx.structure,
+    ensureDependencies: (ctx: ThemeDataContext) => {
+        return ctx.structure ? InteractionsProvider.attach(ctx.structure.root) : Task.empty()
+    }
+}

+ 3 - 3
src/tests/browser/render-structure.ts

@@ -23,7 +23,7 @@ import { MarkerAction } from '../../mol-util/marker-action';
 import { EveryLoci } from '../../mol-model/loci';
 import { lociLabel } from '../../mol-theme/label';
 import { InteractionsRepresentationProvider } from '../../mol-repr/structure/representation/interactions';
-import { ComputedInteractions } from '../../mol-model-props/computed/interactions';
+import { InteractionsProvider } from '../../mol-model-props/computed/interactions';
 
 const parent = document.getElementById('app')!
 parent.style.width = '100%'
@@ -128,9 +128,9 @@ async function init() {
     // console.log(ComputedValenceModel.get(structure))
 
     console.time('computeInteractions')
-    await ComputedInteractions.attachFromCifOrCompute(structure)
+    await InteractionsProvider.attach(structure).run()
     console.timeEnd('computeInteractions');
-    console.log(ComputedInteractions.get(structure))
+    console.log(InteractionsProvider.getValue(structure).value)
 
     const show = {
         cartoon: true,