Browse Source

wip, interactions & hydrogen bonds

Alexander Rose 5 years ago
parent
commit
a86438a265

+ 13 - 2
src/mol-model-props/computed/chemistry/util.ts

@@ -7,6 +7,7 @@
 import { Structure, Unit } from '../../../mol-model/structure';
 import { StructureElement } from '../../../mol-model/structure/structure';
 import { Elements } from '../../../mol-model/structure/model/properties/atomic/types';
+import { BondType } from '../../../mol-model/structure/model/types';
 
 export function typeSymbol(unit: Unit.Atomic, index: StructureElement.UnitIndex) {
     return unit.model.atomicHierarchy.atoms.type_symbol.value(unit.elements[index])
@@ -54,13 +55,23 @@ export function bondToElementCount(structure: Structure, unit: Unit.Atomic, inde
 //
 
 export function intraConnectedTo(unit: Unit.Atomic, indexA: StructureElement.UnitIndex, indexB: StructureElement.UnitIndex) {
-    const { offset, b } = unit.bonds
+    const { offset, b, edgeProps: { flags } } = unit.bonds
+    BondType.is
     for (let i = offset[indexA], il = offset[indexA + 1]; i < il; ++i) {
-        if (b[i] === indexB) return true
+        if (b[i] === indexB && BondType.isCovalent(flags[i])) return true
     }
     return false
 }
 
+export function interConnectedTo(structure: Structure, unitA: Unit.Atomic, indexA: StructureElement.UnitIndex, unitB: Unit.Atomic, indexB: StructureElement.UnitIndex) {
+    const b = structure.interUnitBonds.getEdge(indexA, unitA, indexB, unitB)
+    return b && BondType.isCovalent(b.props.flag)
+}
+
+export function connectedTo(structure: Structure, unitA: Unit.Atomic, indexA: StructureElement.UnitIndex, unitB: Unit.Atomic, indexB: StructureElement.UnitIndex) {
+    return unitA === unitB ? intraConnectedTo(unitA, indexA, indexB) : interConnectedTo(structure, unitA, indexA, unitB, indexB)
+}
+
 //
 
 export function eachInterBondedAtom(structure: Structure, unit: Unit.Atomic, index: StructureElement.UnitIndex, cb: (unit: Unit.Atomic, index: StructureElement.UnitIndex) => void): void {

+ 3 - 3
src/mol-model-props/computed/interactions.ts

@@ -7,7 +7,7 @@
 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 { computeInteractions, Interactions, InteractionsParams as _InteractionsParams } from './interactions/interactions';
 import { CustomStructureProperty } from '../common/custom-property-registry';
 
 export const InteractionsParams = {
@@ -16,7 +16,7 @@ export const InteractionsParams = {
 export type InteractionsParams = typeof InteractionsParams
 export type InteractionsProps = PD.Values<InteractionsParams>
 
-export type InteractionsValue = Map<number, Interactions>
+export type InteractionsValue = Interactions
 
 export const InteractionsProvider: CustomStructureProperty.Provider<InteractionsParams, InteractionsValue> = CustomStructureProperty.createProvider({
     label: 'Interactions',
@@ -31,6 +31,6 @@ export const InteractionsProvider: CustomStructureProperty.Provider<Interactions
     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)
+        return await computeInteractions(ctx, data, p)
     }
 })

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

@@ -0,0 +1,99 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Features } from './features';
+import { IntAdjacencyGraph } from '../../../mol-math/graph';
+import { InteractionType, InteractionsIntraLinks, InteractionsInterLinks } from './common';
+import { Unit, StructureElement } from '../../../mol-model/structure/structure';
+import { InterUnitGraph } from '../../../mol-math/graph/inter-unit-graph';
+import { UniqueArray } from '../../../mol-data/generic';
+
+export { IntraLinksBuilder }
+
+interface IntraLinksBuilder {
+    add: (indexA: number, indexB: number, type: InteractionType) => void
+    getLinks: () => InteractionsIntraLinks
+}
+
+namespace IntraLinksBuilder {
+    export function create(features: Features, elementsCount: number): IntraLinksBuilder {
+        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
+            },
+            getLinks() {
+                const builder = new IntAdjacencyGraph.EdgeBuilder(features.count, aIndices, bIndices)
+                const type = new Int8Array(builder.slotCount) as ArrayLike<InteractionType>
+                for (let i = 0, _i = builder.edgeCount; i < _i; i++) {
+                    builder.addNextEdge()
+                    builder.assignProperty(type, types[i])
+                }
+                return builder.createGraph({ type })
+            }
+        }
+    }
+}
+
+export { InterLinksBuilder }
+
+interface InterLinksBuilder {
+    startUnitPair: (unitA: Unit, unitB: Unit) => void
+    finishUnitPair: () => void
+    add: (indexA: number, indexB: number, type: InteractionType) => void
+    getLinks: () => InteractionsInterLinks
+}
+
+namespace InterLinksBuilder {
+    function addMapEntry<A, B>(map: Map<A, B[]>, a: A, b: B) {
+        if (map.has(a)) map.get(a)!.push(b);
+        else map.set(a, [b]);
+    }
+
+    export function create(): InterLinksBuilder {
+        let uA: Unit
+        let uB: Unit
+        let mapAB: Map<number, InteractionsInterLinks.Info[]>
+        let mapBA: Map<number, InteractionsInterLinks.Info[]>
+        let bondedA: UniqueArray<StructureElement.UnitIndex, StructureElement.UnitIndex>
+        let bondedB: UniqueArray<StructureElement.UnitIndex, StructureElement.UnitIndex>
+        let bondCount: number
+
+        const map = new Map<number, InteractionsInterLinks.Pair[]>();
+
+        return {
+            startUnitPair(unitA: Unit, unitB: Unit) {
+                uA = unitA
+                uB = unitB
+                mapAB = new Map()
+                mapBA = new Map()
+                bondedA = UniqueArray.create()
+                bondedB = UniqueArray.create()
+                bondCount = 0
+            },
+            finishUnitPair() {
+                if (bondCount === 0) return
+                addMapEntry(map, uA.id, new InteractionsInterLinks.Pair(uA, uB, bondCount, bondedA.array, mapAB))
+                addMapEntry(map, uB.id, new InteractionsInterLinks.Pair(uB, uA, bondCount, bondedB.array, mapBA))
+            },
+            add(indexA: number, indexB: number, type: InteractionType) {
+                addMapEntry(mapAB, indexA, { indexB, props: { type } })
+                addMapEntry(mapBA, indexB, { indexB: indexA, props: { type } })
+                UniqueArray.add(bondedA, indexA, indexA)
+                UniqueArray.add(bondedB, indexB, indexB)
+                bondCount += 1
+            },
+            getLinks() {
+                return new InterUnitGraph(map);
+            }
+        }
+    }
+}

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

@@ -0,0 +1,63 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { IntAdjacencyGraph } from '../../../mol-math/graph'
+import { InterUnitGraph } from '../../../mol-math/graph/inter-unit-graph'
+import { Unit } from '../../../mol-model/structure'
+
+export type InteractionsIntraLinks = IntAdjacencyGraph<{ readonly type: ArrayLike<InteractionType> }>
+
+export { InteractionsInterLinks }
+type InteractionsInterLinks = InterUnitGraph<Unit, number, { type: InteractionType }>
+namespace InteractionsInterLinks {
+    export class Pair extends InterUnitGraph.UnitPairEdges<Unit, number, { type: InteractionType }> {}
+    export type Info = InterUnitGraph.EdgeInfo<number, { type: InteractionType }>
+}
+
+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 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
+}

+ 6 - 33
src/mol-model-props/computed/interactions/features.ts

@@ -8,6 +8,7 @@ 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';
+import { FeatureGroup, FeatureType } from './common';
 
 export { Features }
 
@@ -30,10 +31,13 @@ interface Features {
 }
 
 namespace Features {
+    /** Index into Features data arrays */
+    export type FeatureIndex = { readonly '@type': 'feature-index' } & number
+
     /** maps unit elements to features, range for unit element i is offsets[i] to offsets[i + 1] */
     export type ElementsIndex = {
         /** feature indices */
-        readonly indices: ArrayLike<number>
+        readonly indices: ArrayLike<FeatureIndex>
         /** range for unit element i is offsets[i] to offsets[i + 1] */
         readonly offsets: ArrayLike<number>
     }
@@ -62,7 +66,7 @@ namespace Features {
             }
         }
 
-        return { indices, offsets }
+        return { indices: indices as unknown as ArrayLike<FeatureIndex>, offsets }
     }
 }
 
@@ -134,35 +138,4 @@ namespace FeaturesBuilder {
             }
         }
     }
-}
-
-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
 }

+ 140 - 55
src/mol-model-props/computed/interactions/hydrogen-bonds.ts

@@ -7,13 +7,15 @@
 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 { FeaturesBuilder, 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 { typeSymbol, bondToElementCount, bondCount, formalCharge, atomId, compId, altLoc, connectedTo } 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';
+import { FeatureType, FeatureGroup, InteractionType } from './common';
+import { IntraLinksBuilder, InterLinksBuilder } from './builder';
+import { Mat4, Vec3 } from '../../../mol-math/linear-algebra';
 
 export interface HydrogenBonds {
 
@@ -25,7 +27,7 @@ export const HydrogenBondsParams = {
     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 }),
+    maxHbondDonOutOfPlaneAngle: PD.Numeric(45, { min: 0, max: 180, step: 1 }),
 }
 export type HydrogenBondsParams = typeof HydrogenBondsParams
 export type HydrogenBondsProps = PD.Values<HydrogenBondsParams>
@@ -210,78 +212,161 @@ function getHydrogenBondType(unitA: Unit.Atomic, indexA: StructureElement.UnitIn
     }
 }
 
-/**
- * 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
+interface Info {
+    unit: Unit.Atomic,
+    types: ArrayLike<FeatureType>,
+    feature: number,
+    members: ArrayLike<StructureElement.UnitIndex>,
+    offsets: ArrayLike<number>,
+    idealGeometry: Int8Array
+}
+function Info(structure: Structure, unit: Unit.Atomic, features: Features) {
+    const valenceModel = ValenceModelProvider.getValue(structure).value
+    if (!valenceModel || !valenceModel.has(unit.id)) throw new Error('valence model required')
+
+    return {
+        unit,
+        types: features.types,
+        members: features.members,
+        offsets: features.offsets,
+        idealGeometry: valenceModel.get(unit.id)!.idealGeometry
+    } as Info
+}
+
+function getOptions(props: HydrogenBondsProps) {
+    return {
+        maxAccAngleDev: degToRad(props.maxHbondAccAngleDev),
+        maxDonAngleDev: degToRad(props.maxHbondDonAngleDev),
+        maxAccOutOfPlaneAngle: degToRad(props.maxHbondAccOutOfPlaneAngle),
+        maxDonOutOfPlaneAngle: degToRad(props.maxHbondDonOutOfPlaneAngle),
+        maxDist: Math.max(props.maxHbondDist, props.maxHbondSulfurDist),
+        maxHbondDistSq: props.maxHbondDist * props.maxHbondDist,
+    }
+}
+type Options = ReturnType<typeof getOptions>
 
-    const maxAccAngleDev = degToRad(maxHbondAccAngleDev)
-    const maxDonAngleDev = degToRad(maxHbondDonAngleDev)
-    const maxAccOutOfPlaneAngle = degToRad(maxHbondAccOutOfPlaneAngle)
-    const maxDonOutOfPlaneAngle = degToRad(maxHbondDonOutOfPlaneAngle)
+function testHydrogenBond(dSq: number, structure: Structure, infoA: Info, infoB: Info, opts: Options): InteractionType | undefined {
 
-    const maxDist = Math.max(maxHbondDist, maxHbondSulfurDist)
-    const maxHbondDistSq = maxHbondDist * maxHbondDist
+    const typeA = infoA.types[infoA.feature]
+    const typeB = infoB.types[infoB.feature]
 
-    const { x, y, z, count: n, types, offsets, members, lookup3d } = features
+    const isWeak = isWeakHydrogenBond(typeA, typeB)
+    if (!isWeak && !isHydrogenBond(typeA, typeB)) return
 
-    const valenceModel = ValenceModelProvider.getValue(structure).value
-    if (!valenceModel || !valenceModel.has(unit.id)) throw new Error('valence model required')
+    const [don, acc] = typeA === FeatureType.HydrogenAcceptor ? [infoA, infoB] : [infoB, infoA]
 
-    const { idealGeometry } = valenceModel.get(unit.id)!
+    const donIndex = don.members[don.offsets[don.feature]]
+    const accIndex = acc.members[acc.offsets[acc.feature]]
 
-    for (let i = 0; i < n; ++i) {
-        const { count, indices, squaredDistances } = lookup3d.find(x[i], y[i], z[i], maxDist)
-        const ti = types[i]
+    if (accIndex === donIndex) return // DA to self
+
+    const altD = altLoc(don.unit, donIndex)
+    const altA = altLoc(acc.unit, accIndex)
+
+    if (altD && altA && altD !== altA) return // incompatible alternate location id
+    if (don.unit.residueIndex[donIndex] === acc.unit.residueIndex[accIndex]) return // same residue
+
+    // check if distance is ok for non-sulfur-containing hbond
+    if (typeSymbol(don.unit, donIndex) !== Elements.S && typeSymbol(acc.unit, accIndex) !== Elements.S && dSq > opts.maxHbondDistSq) return
+
+    // no hbond if donor and acceptor are bonded
+    if (connectedTo(structure, don.unit, donIndex, acc.unit, accIndex)) return
+
+    const donAngles = calcAngles(structure, don.unit, donIndex, acc.unit, accIndex)
+    const idealDonAngle = AtomGeometryAngles.get(don.idealGeometry[donIndex]) || degToRad(120)
+    if (donAngles.some(donAngle => Math.abs(idealDonAngle - donAngle) > opts.maxDonAngleDev)) return
+
+    if (don.idealGeometry[donIndex] === AtomGeometry.Trigonal) {
+        const outOfPlane = calcPlaneAngle(structure, don.unit, donIndex, acc.unit, accIndex)
+        if (outOfPlane !== undefined && outOfPlane > opts.maxDonOutOfPlaneAngle) return
+    }
+
+    const accAngles = calcAngles(structure, acc.unit, accIndex, don.unit, donIndex)
+    const idealAccAngle = AtomGeometryAngles.get(acc.idealGeometry[accIndex]) || degToRad(120)
+
+    // Do not limit large acceptor angles
+    if (accAngles.some(accAngle => idealAccAngle - accAngle > opts.maxAccAngleDev)) return
+
+    if (acc.idealGeometry[accIndex] === AtomGeometry.Trigonal) {
+        const outOfPlane = calcPlaneAngle(structure, acc.unit, accIndex, don.unit, donIndex)
+        if (outOfPlane !== undefined && outOfPlane > opts.maxAccOutOfPlaneAngle) return
+    }
+
+    return isWeak ? InteractionType.WeakHydrogenBond : getHydrogenBondType(don.unit, donIndex, acc.unit, accIndex)
+}
+
+/**
+ * All intra-unit pairs of hydrogen donor and acceptor atoms
+ */
+export function addUnitHydrogenBonds(structure: Structure, unit: Unit.Atomic, features: Features, builder: IntraLinksBuilder, props: HydrogenBondsProps) {
+    const opts = getOptions(props)
+    const { x, y, z, count, lookup3d } = features
+
+    const infoA = Info(structure, unit, features)
+    const infoB = { ...infoA }
+
+    for (let i = 0; i < count; ++i) {
+        const { count, indices, squaredDistances } = lookup3d.find(x[i], y[i], z[i], opts.maxDist)
+        if (count === 0) continue
+
+        infoA.feature = 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
+            infoB.feature = j
+            const bondType = testHydrogenBond(squaredDistances[r], structure, infoA, infoB, opts)
+            if (bondType) builder.add(i, j, bondType)
+        }
+    }
+}
+
+//
 
-            const [ l, k ] = tj === FeatureType.HydrogenAcceptor ? [ i, j ] : [ j, i ]
+const _imageTransform = Mat4()
 
-            const donorIdx = members[offsets[l]]
-            const acceptorIdx = members[offsets[k]]
+/**
+ * All inter-unit pairs of hydrogen donor and acceptor atoms
+ */
+export function addStructureHydrogenBonds(structure: Structure, unitA: Unit.Atomic, featuresA: Features, unitB: Unit.Atomic, featuresB: Features, builder: InterLinksBuilder, props: HydrogenBondsProps) {
+    const opts = getOptions(props)
 
-            if (acceptorIdx === donorIdx) continue // DA to self
+    const { count, x: xA, y: yA, z: zA } = featuresA;
+    const { lookup3d } = featuresB;
 
-            const altD = altLoc(unit, donorIdx)
-            const altA = altLoc(unit, acceptorIdx)
+    // the lookup queries need to happen in the "unitB space".
+    // that means imageA = inverseOperB(operA(i))
+    const imageTransform = Mat4.mul(_imageTransform, unitB.conformation.operator.inverse, unitA.conformation.operator.matrix)
+    const isNotIdentity = !Mat4.isIdentity(imageTransform)
+    const imageA = Vec3()
 
-            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 { center: bCenter, radius: bRadius } = lookup3d.boundary.sphere;
+    const testDistanceSq = (bRadius + opts.maxDist) * (bRadius + opts.maxDist);
 
-            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
+    const infoA = Info(structure, unitA, featuresA)
+    const infoB = Info(structure, unitB, featuresB)
 
-            if (idealGeometry[donorIdx] === AtomGeometry.Trigonal) {
-                const outOfPlane = calcPlaneAngle(structure, unit, donorIdx, unit, acceptorIdx)
-                if (outOfPlane !== undefined && outOfPlane > maxDonOutOfPlaneAngle) continue
-            }
+    builder.startUnitPair(unitA, unitB)
 
-            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
+    for (let i = 0; i < count; ++i) {
+        Vec3.set(imageA, xA[i], yA[i], zA[i])
+        if (isNotIdentity) Vec3.transformMat4(imageA, imageA, imageTransform)
+        if (Vec3.squaredDistance(imageA, bCenter) > testDistanceSq) continue
 
-            if (idealGeometry[acceptorIdx] === AtomGeometry.Trigonal) {
-                const outOfPlane = calcPlaneAngle(structure, unit, acceptorIdx, unit, donorIdx)
-                if (outOfPlane !== undefined && outOfPlane > maxAccOutOfPlaneAngle) continue
-            }
+        const { indices, count, squaredDistances } = lookup3d.find(imageA[0], imageA[1], imageA[2], opts.maxDist)
+        if (count === 0) continue
 
-            const bondType = isWeak ? InteractionType.WeakHydrogenBond : getHydrogenBondType(unit, donorIdx, unit, acceptorIdx)
-            builder.add(l, k, bondType)
+        infoA.feature = i
+
+        for (let r = 0; r < count; ++r) {
+            const j = indices[r]
+            if (j <= i) continue
+            infoB.feature = j
+            const bondType = testHydrogenBond(squaredDistances[r], structure, infoA, infoB, opts)
+            if (bondType) builder.add(i, j, bondType)
         }
     }
+
+    builder.finishUnitPair()
 }

+ 162 - 87
src/mol-model-props/computed/interactions/interactions.ts

@@ -5,120 +5,195 @@
  */
 
 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 { Structure, Unit } from '../../../mol-model/structure';
+import { RuntimeContext } from '../../../mol-task';
+import { addUnitHydrogenDonors, addUnitWeakHydrogenDonors, addUnitHydrogenAcceptors, addUnitHydrogenBonds, HydrogenBondsParams, addStructureHydrogenBonds } 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
+import { InteractionsIntraLinks, InteractionsInterLinks, InteractionType } from './common';
+import { IntraLinksBuilder, InterLinksBuilder } from './builder';
+import { IntMap } from '../../../mol-data/int';
+import { Vec3 } from '../../../mol-math/linear-algebra';
+
+export { Interactions }
+
+interface Interactions {
+    /** Features of each unit */
+    unitsFeatures: IntMap<Features>
+    /** Interactions of each unit */
+    unitsLinks: IntMap<InteractionsIntraLinks>
+    /** Interactions between units */
+    links: InteractionsInterLinks
 }
 
-export { InteractionsBuilder }
+namespace Interactions {
+    export interface Location {
+        readonly kind: 'interaction-location'
+        interactions: Interactions
+        unitA: Unit
+        /** Index into features of unitA */
+        indexA: number
+        unitB: Unit
+        /** Index into features of unitB */
+        indexB: number
+    }
 
-interface InteractionsBuilder {
-    add: (indexA: number, indexB: number, type: InteractionType) => void
-    getInteractions: () => Interactions
-}
+    export function Location(interactions?: Interactions, unitA?: Unit, indexA?: number, unitB?: Unit, indexB?: number): Location {
+        return { kind: 'interaction-location', interactions: interactions as any, unitA: unitA as any, indexA: indexA as any, unitB: unitB as any, indexB: indexB as any };
+    }
 
-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,
-                    elementsIndex,
-                    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 function isLocation(x: any): x is Location {
+        return !!x && x.kind === 'interaction-location';
+    }
+
+    export function areLocationsEqual(locA: Location, locB: Location) {
+        return (
+            locA.interactions === locB.interactions &&
+            locA.indexA === locB.indexA && locA.indexB === locB.indexB &&
+            locA.unitA === locB.unitA && locA.unitB === locB.unitB
+        )
+    }
+
+    export interface Loci {
+        readonly kind: 'interaction-loci'
+        readonly structure: Structure
+        readonly interactions: Interactions
+        readonly links: ReadonlyArray<{
+            unitA: Unit
+            /** Index into features of unitA */
+            indexA: number
+            unitB: Unit
+            /** Index into features of unitB */
+            indexB: number
+        }>
+    }
+
+    export function Loci(structure: Structure, interactions: Interactions, links: Loci['links']): Loci {
+        return { kind: 'interaction-loci', structure, interactions, links };
+    }
+
+    export function isLoci(x: any): x is Loci {
+        return !!x && x.kind === 'interaction-loci';
+    }
+
+    export function areLociEqual(a: Loci, b: Loci) {
+        if (a.structure !== b.structure) return false
+        if (a.interactions !== b.interactions) return false
+        if (a.links.length !== b.links.length) return false
+        for (let i = 0, il = a.links.length; i < il; ++i) {
+            const linkA = a.links[i]
+            const linkB = b.links[i]
+            if (linkA.unitA !== linkB.unitA) return false
+            if (linkA.unitB !== linkB.unitB) return false
+            if (linkA.indexA !== linkB.indexA) return false
+            if (linkA.indexB !== linkB.indexB) return false
         }
+        return true
     }
-}
 
-export type InteractionsLinks = IntAdjacencyGraph<{ readonly types: ArrayLike<InteractionType> }>
+    export function isLociEmpty(loci: Loci) {
+        return loci.links.length === 0 ? true : false
+    }
 
-export interface Interactions {
-    links: InteractionsLinks
-    features: Features
-    elementsIndex: Features.ElementsIndex
-    getLinkIndex: (indexA: StructureElement.UnitIndex, indexB: StructureElement.UnitIndex) => number
+    export function typeLabel(type: InteractionType): string {
+        switch (type) {
+            case InteractionType.HydrogenBond:
+            case InteractionType.WaterHydrogenBond:
+            case InteractionType.BackboneHydrogenBond:
+                return 'Hydrogen Bond'
+            case InteractionType.Hydrophobic:
+                return 'Hydrophobic Contact'
+            case InteractionType.HalogenBond:
+                return 'Halogen Bond'
+            case InteractionType.IonicInteraction:
+                return 'Ionic Interaction'
+            case InteractionType.MetalCoordination:
+                return 'Metal Coordination'
+            case InteractionType.CationPi:
+                return 'Cation-Pi Interaction'
+            case InteractionType.PiStacking:
+                return 'Pi-Pi Stacking'
+            case InteractionType.WeakHydrogenBond:
+                return 'Weak Hydrogen Bond'
+            case InteractionType.Unknown:
+                return 'Unknown Interaction'
+        }
+    }
 }
 
 export const InteractionsParams = {
-    ...HydrogenBondsParams
+    ...HydrogenBondsParams,
 }
 export type InteractionsParams = typeof InteractionsParams
 export type InteractionsProps = PD.Values<InteractionsParams>
 
-export async function calcInteractions(runtime: RuntimeContext, structure: Structure, props: Partial<InteractionsProps>) {
+export async function computeInteractions(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>()
+
+    const unitsFeatures = IntMap.Mutable<Features>()
+    const unitsLinks = IntMap.Mutable<InteractionsIntraLinks>()
+
     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)
-        }
+        const d = findIntraUnitLinksAndFeatures(structure, u, p)
+        unitsFeatures.set(u.id, d.features)
+        unitsLinks.set(u.id, d.links)
     }
-    return map
+
+    const links = findInterUnitLinks(structure, unitsFeatures, p)
+
+    return { unitsFeatures, unitsLinks, links }
 }
 
-function calcIntraUnitInteractions(structure: Structure, unit: Unit.Atomic, props: InteractionsProps) {
+function findIntraUnitLinksAndFeatures(structure: Structure, unit: Unit, props: InteractionsProps) {
 
     const featuresBuilder = FeaturesBuilder.create()
-    addUnitHydrogenDonors(structure, unit, featuresBuilder)
-    addUnitWeakHydrogenDonors(structure, unit, featuresBuilder)
-    addUnitHydrogenAcceptors(structure, unit, featuresBuilder)
-
+    if (Unit.isAtomic(unit)) {
+        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)
+    const linksBuilder = IntraLinksBuilder.create(features, unit.elements.length)
+    if (Unit.isAtomic(unit)) {
+        addUnitHydrogenBonds(structure, unit, features, linksBuilder, props)
+    }
+
+    return { features, links: linksBuilder.getLinks() }
+}
+
+const MAX_RADIUS = 5
+
+function findInterUnitLinks(structure: Structure, unitsFeatures: IntMap<Features>, props: InteractionsProps) {
+    const builder = InterLinksBuilder.create()
+
+    const lookup = structure.lookup3d;
+    const imageCenter = Vec3.zero();
+
+    for (const unitA of structure.units) {
+        if (!Unit.isAtomic(unitA)) continue;
+
+        const featuresA = unitsFeatures.get(unitA.id)
+
+        const bs = unitA.lookup3d.boundary.sphere;
+        Vec3.transformMat4(imageCenter, bs.center, unitA.conformation.operator.matrix);
+        const closeUnits = lookup.findUnitIndices(imageCenter[0], imageCenter[1], imageCenter[2], bs.radius + MAX_RADIUS);
+
+        for (let i = 0; i < closeUnits.count; i++) {
+            const unitB = structure.units[closeUnits.indices[i]];
+            if (!Unit.isAtomic(unitB) || unitA.id >= unitB.id || !Structure.validUnitPair(structure, unitA, unitB)) continue;
+
+            const featuresB = unitsFeatures.get(unitB.id)
+
+            if (unitB.elements.length >= unitA.elements.length) {
+                addStructureHydrogenBonds(structure, unitA, featuresA, unitB, featuresB, builder, props)
+            } else {
+                addStructureHydrogenBonds(structure, unitB, featuresB, unitA, featuresA, builder, props)
+            }
+        }
+    }
 
-    return interactionsBuilder.getInteractions()
+    return builder.getLinks()
 }

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

@@ -7,6 +7,7 @@
 import { StructureElement } from './structure'
 import { Bond } from './structure/structure/unit/bonds'
 import { ShapeGroup } from './shape/shape';
+import { Interactions } from '../mol-model-props/computed/interactions/interactions';
 
 /** A null value Location */
 export const NullLocation = { kind: 'null-location' as 'null-location' }
@@ -15,4 +16,4 @@ export function isNullLocation(x: any): x is NullLocation {
     return !!x && x.kind === 'null-location';
 }
 
-export type Location = StructureElement.Location | Bond.Location | ShapeGroup.Location | NullLocation
+export type Location = StructureElement.Location | Bond.Location | Interactions.Location | ShapeGroup.Location | NullLocation

+ 15 - 1
src/mol-model/loci.ts

@@ -14,6 +14,7 @@ import { OrderedSet } from '../mol-data/int';
 import { Structure } from './structure/structure';
 import { PrincipalAxes } from '../mol-math/linear-algebra/matrix/principal-axes';
 import { ParamDefinition } from '../mol-util/param-definition';
+import { Interactions } from '../mol-model-props/computed/interactions/interactions';
 
 /** A Loci that includes every loci */
 export const EveryLoci = { kind: 'every-loci' as 'every-loci' }
@@ -51,7 +52,7 @@ export function createDataLoci(data: any, tag: string, indices: OrderedSet<numbe
 
 export { Loci }
 
-type Loci = StructureElement.Loci | Structure.Loci | Bond.Loci | EveryLoci | EmptyLoci | DataLoci | Shape.Loci | ShapeGroup.Loci
+type Loci = StructureElement.Loci | Structure.Loci | Bond.Loci | Interactions.Loci | EveryLoci | EmptyLoci | DataLoci | Shape.Loci | ShapeGroup.Loci
 
 namespace Loci {
     interface FiniteArray<T, L extends number = number> extends ReadonlyArray<T> { length: L };
@@ -72,6 +73,9 @@ namespace Loci {
         if (Bond.isLoci(lociA) && Bond.isLoci(lociB)) {
             return Bond.areLociEqual(lociA, lociB)
         }
+        if (Interactions.isLoci(lociA) && Interactions.isLoci(lociB)) {
+            return Interactions.areLociEqual(lociA, lociB)
+        }
         if (Shape.isLoci(lociA) && Shape.isLoci(lociB)) {
             return Shape.areLociEqual(lociA, lociB)
         }
@@ -92,6 +96,7 @@ namespace Loci {
         if (Structure.isLoci(loci)) return Structure.isLociEmpty(loci)
         if (StructureElement.Loci.is(loci)) return StructureElement.Loci.isEmpty(loci)
         if (Bond.isLoci(loci)) return Bond.isLociEmpty(loci)
+        if (Interactions.isLoci(loci)) return Interactions.isLociEmpty(loci)
         if (Shape.isLoci(loci)) return Shape.isLociEmpty(loci)
         if (ShapeGroup.isLoci(loci)) return ShapeGroup.isLociEmpty(loci)
         return false
@@ -105,6 +110,9 @@ namespace Loci {
                 loci = Structure.remapLoci(loci, data)
             } else if (Bond.isLoci(loci)) {
                 loci = Bond.remapLoci(loci, data)
+            } else if (Interactions.isLoci(loci)) {
+                // TODO might be too expensive
+                // loci = Interactions.remapLoci(loci, data)
             }
         }
         return loci
@@ -136,6 +144,9 @@ namespace Loci {
                 e.aUnit.conformation.position(e.bUnit.elements[e.bIndex], tempPos);
                 sphereHelper.radiusStep(tempPos);
             }
+        } else if (loci.kind === 'interaction-loci') {
+            // TODO
+            // return Interactions.Loci.getBoundary(loci).sphere;
         } else if (loci.kind === 'shape-loci') {
             // TODO
             return void 0;
@@ -168,6 +179,9 @@ namespace Loci {
         } else if (loci.kind === 'bond-loci') {
             // TODO
             return void 0;
+        } else if (loci.kind === 'interaction-loci') {
+            // TODO
+            return void 0;
         } else if (loci.kind === 'shape-loci') {
             // TODO
             return void 0;

+ 2 - 1
src/mol-repr/structure/complex-representation.ts

@@ -17,6 +17,7 @@ import { PickingId } from '../../mol-geo/geometry/picking';
 import { EmptyLoci, Loci, isEveryLoci } from '../../mol-model/loci';
 import { MarkerAction } from '../../mol-util/marker-action';
 import { Overpaint } from '../../mol-theme/overpaint';
+import { Interactions } from '../../mol-model-props/computed/interactions/interactions';
 
 export function ComplexRepresentation<P extends StructureParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, P>, visualCtor: (materialId: number) => ComplexVisual<P>): StructureRepresentation<P> {
     let version = 0
@@ -58,7 +59,7 @@ export function ComplexRepresentation<P extends StructureParams>(label: string,
 
     function mark(loci: Loci, action: MarkerAction) {
         if (!_structure) return false
-        if (Structure.isLoci(loci) || StructureElement.Loci.is(loci) || Bond.isLoci(loci)) {
+        if (Structure.isLoci(loci) || StructureElement.Loci.is(loci) || Bond.isLoci(loci) || Interactions.isLoci(loci)) {
             if (!Structure.areRootsEquivalent(loci.structure, _structure)) return false
             // Remap `loci` from equivalent structure to the current `_structure`
             loci = Loci.remap(loci, _structure)

+ 5 - 3
src/mol-repr/structure/representation/interactions.ts

@@ -8,21 +8,23 @@ 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 { UnitsRepresentation, StructureRepresentation, StructureRepresentationStateBuilder, StructureRepresentationProvider, ComplexRepresentation } 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';
+import { InteractionsInterUnitParams, InteractionsInterUnitVisual } from '../visual/interactions-inter-unit-cylinder';
 
 const InteractionsVisuals = {
     'intra-unit': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, InteractionsIntraUnitParams>) => UnitsRepresentation('Intra-unit interactions cylinder', ctx, getParams, InteractionsIntraUnitVisual),
+    'inter-unit': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, InteractionsInterUnitParams>) => ComplexRepresentation('Inter-unit interactions cylinder', ctx, getParams, InteractionsInterUnitVisual),
 }
 
 export const InteractionsParams = {
     ...InteractionsIntraUnitParams,
+    ...InteractionsInterUnitParams,
     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)),
+    visuals: PD.MultiSelect(['intra-unit', 'inter-unit'], PD.objectToOptions(InteractionsVisuals)),
 }
 export type InteractionsParams = typeof InteractionsParams
 export function getInteractionParams(ctx: ThemeRegistryContext, structure: Structure) {

+ 2 - 1
src/mol-repr/structure/units-representation.ts

@@ -20,6 +20,7 @@ import { PickingId } from '../../mol-geo/geometry/picking';
 import { Loci, EmptyLoci, isEmptyLoci, isEveryLoci } from '../../mol-model/loci';
 import { MarkerAction } from '../../mol-util/marker-action';
 import { Overpaint } from '../../mol-theme/overpaint';
+import { Interactions } from '../../mol-model-props/computed/interactions/interactions';
 
 export const UnitsParams = {
     ...StructureParams,
@@ -167,7 +168,7 @@ export function UnitsRepresentation<P extends UnitsParams>(label: string, ctx: R
     function mark(loci: Loci, action: MarkerAction) {
         let changed = false
         if (!_structure) return false
-        if (Structure.isLoci(loci) || StructureElement.Loci.is(loci) || Bond.isLoci(loci)) {
+        if (Structure.isLoci(loci) || StructureElement.Loci.is(loci) || Bond.isLoci(loci) || Interactions.isLoci(loci)) {
             if (!Structure.areRootsEquivalent(loci.structure, _structure)) return false
             // Remap `loci` from equivalent structure to the current `_structure`
             loci = Loci.remap(loci, _structure)

+ 123 - 0
src/mol-repr/structure/visual/interactions-inter-unit-cylinder.ts

@@ -0,0 +1,123 @@
+/**
+ * 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 { VisualContext } from '../../visual';
+import { Structure } from '../../../mol-model/structure';
+import { Theme } from '../../../mol-theme/theme';
+import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
+import { Vec3 } from '../../../mol-math/linear-algebra';
+import { createBondCylinderMesh, BondCylinderParams } from './util/bond';
+import { ComplexMeshParams, ComplexVisual, ComplexMeshVisual } from '../complex-visual';
+import { VisualUpdateState } from '../../util';
+import { PickingId } from '../../../mol-geo/geometry/picking';
+import { EmptyLoci, Loci } from '../../../mol-model/loci';
+import { Interval } from '../../../mol-data/int';
+import { BondType } from '../../../mol-model/structure/model/types';
+import { Interactions } from '../../../mol-model-props/computed/interactions/interactions';
+import { InteractionsProvider } from '../../../mol-model-props/computed/interactions';
+import { LocationIterator } from '../../../mol-geo/util/location-iterator';
+
+function createInterUnitInteractionCylinderMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<InteractionsInterUnitParams>, mesh?: Mesh) {
+    if (!structure.hasAtomic) return Mesh.createEmpty(mesh)
+
+    const interactions = InteractionsProvider.getValue(structure).value!
+    const { links, unitsFeatures } = interactions
+
+    const { edgeCount, edges } = links
+    const { sizeFactor } = props
+
+    if (!edgeCount) return Mesh.createEmpty(mesh)
+
+    const builderProps = {
+        bondCount: edgeCount,
+        referencePosition: () => null,
+        position: (posA: Vec3, posB: Vec3, edgeIndex: number) => {
+            const { unitA, indexA, unitB, indexB } = edges[edgeIndex]
+            const fA = unitsFeatures.get(unitA.id)
+            const fB = unitsFeatures.get(unitB.id)
+            Vec3.set(posA, fA.x[indexA], fA.y[indexA], fA.z[indexA])
+            Vec3.set(posB, fB.x[indexB], fB.y[indexB], fB.z[indexB])
+        },
+        order: (edgeIndex: number) => 1,
+        flags: (edgeIndex: number) => BondType.Flag.MetallicCoordination, // TODO
+        radius: (edgeIndex: number) => sizeFactor,
+        ignore: () => false
+    }
+
+    return createBondCylinderMesh(ctx, builderProps, props, mesh)
+}
+
+export const InteractionsInterUnitParams = {
+    ...ComplexMeshParams,
+    ...BondCylinderParams,
+    sizeFactor: PD.Numeric(0.3, { min: 0, max: 10, step: 0.01 }),
+}
+export type InteractionsInterUnitParams = typeof InteractionsInterUnitParams
+
+export function InteractionsInterUnitVisual(materialId: number): ComplexVisual<InteractionsInterUnitParams> {
+    return ComplexMeshVisual<InteractionsInterUnitParams>({
+        defaultProps: PD.getDefaultValues(InteractionsInterUnitParams),
+        createGeometry: createInterUnitInteractionCylinderMesh,
+        createLocationIterator: createInteractionsIterator,
+        getLoci: getInteractionLoci,
+        eachLocation: eachInteraction,
+        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<InteractionsInterUnitParams>, currentProps: PD.Values<InteractionsInterUnitParams>) => {
+            state.createGeometry = (
+                newProps.sizeFactor !== currentProps.sizeFactor ||
+                newProps.radialSegments !== currentProps.radialSegments
+            )
+        }
+    }, materialId)
+}
+
+function getInteractionLoci(pickingId: PickingId, structure: Structure, id: number) {
+    const { objectId, groupId } = pickingId
+    if (id === objectId) {
+        const interactions = InteractionsProvider.getValue(structure).value!
+        const l = interactions.links.edges[groupId]
+        return Interactions.Loci(structure, interactions, [
+            { unitA: l.unitA, indexA: l.indexA, unitB: l.unitB, indexB: l.indexB },
+            { unitA: l.unitB, indexA: l.indexB, unitB: l.unitA, indexB: l.indexA },
+        ])
+    }
+    return EmptyLoci
+}
+
+function eachInteraction(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) {
+    let changed = false
+    if (Interactions.isLoci(loci)) {
+        if (!Structure.areEquivalent(loci.structure, structure)) return false
+        const interactions = InteractionsProvider.getValue(structure).value!
+        if (loci.interactions !== interactions) return false
+        const { links } = interactions
+
+        for (const l of loci.links) {
+            const idx = links.getEdgeIndex(l.indexA, l.unitA, l.indexB, l.unitB)
+            if (idx !== -1) {
+                if (apply(Interval.ofSingleton(idx))) changed = true
+            }
+        }
+    }
+    return changed
+}
+
+function createInteractionsIterator(structure: Structure): LocationIterator {
+    const interactions = InteractionsProvider.getValue(structure).value!
+    const { links } = interactions
+    const groupCount = links.edgeCount
+    const instanceCount = 1
+    const location = Interactions.Location(interactions)
+    const getLocation = (groupIndex: number) => {
+        const link = links.edges[groupIndex]
+        location.unitA = link.unitA
+        location.indexA = link.indexA
+        location.unitB = link.unitB
+        location.indexB = link.indexB
+        return location
+    }
+    return LocationIterator(groupCount, instanceCount, getLocation, true)
+}

+ 27 - 63
src/mol-repr/structure/visual/interactions-intra-unit-cylinder.ts

@@ -4,10 +4,10 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Unit, Bond, Structure, StructureElement } from '../../../mol-model/structure';
+import { Unit, Structure } from '../../../mol-model/structure';
 import { Vec3 } from '../../../mol-math/linear-algebra';
 import { Loci, EmptyLoci } from '../../../mol-model/loci';
-import { Interval, OrderedSet } from '../../../mol-data/int';
+import { Interval } 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';
@@ -19,12 +19,14 @@ import { createBondCylinderMesh, BondCylinderParams } from './util/bond';
 import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual, StructureGroup } from '../units-visual';
 import { VisualUpdateState } from '../../util';
 import { LocationIterator } from '../../../mol-geo/util/location-iterator';
+import { Interactions } from '../../../mol-model-props/computed/interactions/interactions';
 
 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 features = interactions.unitsFeatures.get(unit.id)
+    const links = interactions.unitsLinks.get(unit.id)
 
     const { x, y, z } = features
     const { edgeCount, a, b } = links
@@ -60,86 +62,51 @@ export function InteractionsIntraUnitVisual(materialId: number): UnitsVisual<Int
         defaultProps: PD.getDefaultValues(InteractionsIntraUnitParams),
         createGeometry: createIntraUnitInteractionsCylinderMesh,
         createLocationIterator: createInteractionsIterator,
-        getLoci: getLinkLoci,
+        getLoci: getInteractionLoci,
         eachLocation: eachInteraction,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<InteractionsIntraUnitParams>, currentProps: PD.Values<InteractionsIntraUnitParams>) => {
             state.createGeometry = (
                 newProps.sizeFactor !== currentProps.sizeFactor ||
-                newProps.radialSegments !== currentProps.radialSegments ||
-                newProps.bondScale !== currentProps.bondScale ||
-                newProps.bondSpacing !== currentProps.bondSpacing
+                newProps.radialSegments !== currentProps.radialSegments
             )
         }
     }, materialId)
 }
 
-function getLinkLoci(pickingId: PickingId, structureGroup: StructureGroup, id: number) {
+function getInteractionLoci(pickingId: PickingId, structureGroup: StructureGroup, id: number) {
     const { objectId, instanceId, groupId } = pickingId
     if (id === objectId) {
         const { structure, group } = structureGroup
         const unit = structure.unitMap.get(group.units[instanceId].id)
-        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 Bond.Loci(structure, [
-                Bond.Location(
-                    unit, members[offsets[links.a[groupId]]],
-                    unit, members[offsets[links.b[groupId]]]
-                ),
-                Bond.Location(
-                    unit, members[offsets[links.b[groupId]]],
-                    unit, members[offsets[links.a[groupId]]]
-                )
-            ])
-        }
+        const interactions = InteractionsProvider.getValue(structure).value!
+        const links = interactions.unitsLinks.get(unit.id)
+        return Interactions.Loci(structure, interactions, [
+            { unitA: unit, indexA: links.a[groupId], unitB: unit, indexB: links.b[groupId] },
+            { unitA: unit, indexA: links.b[groupId], unitB: unit, indexB: links.a[groupId] },
+        ])
     }
     return EmptyLoci
 }
 
 function eachInteraction(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) {
     let changed = false
-    if (Bond.isLoci(loci)) {
+    if (Interactions.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)!
+        if (loci.interactions !== interactions) return false
+        const unit = group.units[0]
+        const links = interactions.unitsLinks.get(unit.id)
         const groupCount = links.edgeCount * 2
-        for (const b of loci.bonds) {
-            const unitIdx = group.unitIndexMap.get(b.aUnit.id)
+        for (const l of loci.links) {
+            const unitIdx = group.unitIndexMap.get(l.unitA.id)
             if (unitIdx !== undefined) {
-                const idx = getLinkIndex(b.aIndex, b.bIndex)
+                const idx = links.getDirectedEdgeIndex(l.indexA, l.indexB)
                 if (idx !== -1) {
                     if (apply(Interval.ofSingleton(unitIdx * groupCount + idx))) changed = true
                 }
             }
         }
-    } else if (StructureElement.Loci.is(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, elementsIndex } = interactions.get(unit.id)!
-        const groupCount = links.edgeCount * 2
-        for (const e of loci.elements) {
-            const unitIdx = group.unitIndexMap.get(e.unit.id)
-            if (unitIdx !== undefined) {
-                const { offset } = links
-                const { indices, offsets } = elementsIndex
-                OrderedSet.forEach(e.indices, v => {
-                    for (let i = offsets[v], il = offsets[v + 1]; i < il; ++i) {
-                        const f = indices[i]
-                        for (let t = offset[f], _t = offset[f + 1]; t < _t; t++) {
-                            if (apply(Interval.ofSingleton(unitIdx * groupCount + t))) changed = true
-                        }
-                    }
-                })
-            }
-        }
     }
     return changed
 }
@@ -148,19 +115,16 @@ function createInteractionsIterator(structureGroup: StructureGroup): LocationIte
     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 links = interactions.unitsLinks.get(unit.id)
     const groupCount = links.edgeCount * 2
     const instanceCount = group.units.length
-    const location = Bond.Location()
+    const location = Interactions.Location(interactions)
     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]]
+        location.unitA = instanceUnit
+        location.indexA = links.a[groupIndex]
+        location.unitB = instanceUnit
+        location.indexB = links.b[groupIndex]
         return location
     }
     return LocationIterator(groupCount, instanceCount, getLocation)

+ 12 - 11
src/mol-theme/color/interaction-type.ts

@@ -4,16 +4,16 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Bond } 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 { InteractionType } from '../../mol-model-props/computed/interactions/common';
 import { TableLegend } from '../../mol-util/legend';
 import { Task } from '../../mol-task';
+import { Interactions } from '../../mol-model-props/computed/interactions/interactions';
 
 const DefaultColor = Color(0xCCCCCC)
 const Description = 'Assigns colors according the interaction type of a link.'
@@ -78,16 +78,17 @@ export function InteractionTypeColorTheme(ctx: ThemeDataContext, props: PD.Value
     const contextHash = interactions?.version
 
     if (interactions && interactions.value) {
-        const map = interactions.value
         color = (location: Location) => {
-            if (Bond.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])
-                    }
+            if (Interactions.isLocation(location)) {
+                const { interactions, unitA, indexA, unitB, indexB } = location
+                if (location.unitA === location.unitB) {
+                    const links = interactions.unitsLinks.get(location.unitA.id)
+                    const idx = links.getDirectedEdgeIndex(location.indexA, location.indexB)
+                    return typeColor(links.edgeProps.type[idx])
+                } else {
+                    const idx = interactions.links.getEdgeIndex(indexA, unitA, indexB, unitB)
+                    console.log({ idx, indexA, unitA, indexB, unitB })
+                    return typeColor(interactions.links.edges[idx].props.type)
                 }
             }
             return DefaultColor

+ 16 - 0
src/mol-theme/label.ts

@@ -10,6 +10,7 @@ import { Loci } from '../mol-model/loci';
 import { OrderedSet } from '../mol-data/int';
 import { capitalize, stripTags } from '../mol-util/string';
 import { Column } from '../mol-data/db';
+import { Interactions } from '../mol-model-props/computed/interactions/interactions';
 
 export type LabelGranularity = 'element' | 'conformation' | 'residue' | 'chain' | 'structure'
 
@@ -30,6 +31,9 @@ export function lociLabel(loci: Loci, options: Partial<LabelOptions> = {}): stri
         case 'bond-loci':
             const bond = loci.bonds[0]
             return bond ? bondLabel(bond) : 'Unknown'
+        case 'interaction-loci':
+            const link = loci.links[0]
+            return link ? interactionLabel(Interactions.Location(loci.interactions, link.unitA, link.indexA, link.unitB, link.indexB)) : 'Unknown'
         case 'shape-loci':
             return loci.shape.name
         case 'group-loci':
@@ -123,6 +127,18 @@ export function bondLabel(bond: Bond.Location): string {
     return `${labelA.join(' | ')} \u2014 ${labelB.slice(offset).join(' | ')}`
 }
 
+export function interactionLabel(location: Interactions.Location): string {
+    const { interactions, unitA, indexA, unitB, indexB } = location
+    if (location.unitA === location.unitB) {
+        const links = interactions.unitsLinks.get(location.unitA.id)
+        const idx = links.getDirectedEdgeIndex(location.indexA, location.indexB)
+        return Interactions.typeLabel(links.edgeProps.type[idx])
+    } else {
+        const idx = interactions.links.getEdgeIndex(indexA, unitA, indexB, unitB)
+        return Interactions.typeLabel(interactions.links.edges[idx].props.type)
+    }
+}
+
 export function elementLabel(location: StructureElement.Location, options: Partial<LabelOptions> = {}): string {
     const o = { ...DefaultLabelOptions, ...options }
     const label = _elementLabel(location, o.granularity, o.hidePrefix).join(' | ')

+ 7 - 12
src/tests/browser/render-structure.ts

@@ -15,7 +15,6 @@ import { trajectoryFromMmCIF } from '../../mol-model-formats/structure/mmcif';
 import { MolecularSurfaceRepresentationProvider } from '../../mol-repr/structure/representation/molecular-surface';
 import { BallAndStickRepresentationProvider } from '../../mol-repr/structure/representation/ball-and-stick';
 import { GaussianSurfaceRepresentationProvider } from '../../mol-repr/structure/representation/gaussian-surface';
-// import { ComputedSecondaryStructure } from '../../mol-model-props/computed/secondary-structure';
 import { resizeCanvas } from '../../mol-canvas3d/util';
 import { Representation } from '../../mol-repr/representation';
 import { throttleTime } from 'rxjs/operators';
@@ -24,6 +23,7 @@ import { EveryLoci } from '../../mol-model/loci';
 import { lociLabel } from '../../mol-theme/label';
 import { InteractionsRepresentationProvider } from '../../mol-repr/structure/representation/interactions';
 import { InteractionsProvider } from '../../mol-model-props/computed/interactions';
+import { SecondaryStructureProvider } from '../../mol-model-props/computed/secondary-structure';
 
 const parent = document.getElementById('app')!
 parent.style.width = '100%'
@@ -115,21 +115,16 @@ function getGaussianSurfaceRepr() {
 }
 
 async function init() {
-    const cif = await downloadFromPdb('1crn')
+    const cif = await downloadFromPdb('3pqr')
     const models = await getModels(cif)
     const structure = await getStructure(models[0])
-    // console.time('computeDSSP')
-    // await ComputedSecondaryStructure.attachFromCifOrCompute(structure)
-    // console.timeEnd('computeDSSP');
+    console.time('compute SecondaryStructure')
+    await SecondaryStructureProvider.attach(structure).run()
+    console.timeEnd('compute SecondaryStructure');
 
-    // console.time('computeValenceModel')
-    // await ComputedValenceModel.attachFromCifOrCompute(structure)
-    // console.timeEnd('computeValenceModel');
-    // console.log(ComputedValenceModel.get(structure))
-
-    console.time('computeInteractions')
+    console.time('compute Interactions')
     await InteractionsProvider.attach(structure).run()
-    console.timeEnd('computeInteractions');
+    console.timeEnd('compute Interactions');
     console.log(InteractionsProvider.getValue(structure).value)
 
     const show = {