Browse Source

fixed & improved carbohydrate handling

- marking behavior like for polymer visual (single atom triggers full marking)
- fixed carbohydrate links induced by intra-unit bonds
- carbohydrate element is now defined by an altId ring index (instead of the anomeric carbon element index)
Alexander Rose 5 years ago
parent
commit
929c91a48c

+ 135 - 170
src/mol-model/structure/structure/carbohydrates/compute.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author David Sehnal <david.sehnal@gmail.com>
@@ -10,9 +10,9 @@ import { combinations } from '../../../../mol-data/util/combination';
 import { IntAdjacencyGraph } from '../../../../mol-math/graph';
 import { Vec3 } from '../../../../mol-math/linear-algebra';
 import { PrincipalAxes } from '../../../../mol-math/linear-algebra/matrix/principal-axes';
-import { fillSerial } from '../../../../mol-util/array';
+import { fillSerial, arraySetAdd } from '../../../../mol-util/array';
 import { ResidueIndex, Model } from '../../model';
-import { ElementSymbol } from '../../model/types';
+import { ElementSymbol, BondType } from '../../model/types';
 import { getPositions } from '../../util';
 import StructureElement from '../element';
 import Structure from '../structure';
@@ -20,6 +20,7 @@ import Unit from '../unit';
 import { CarbohydrateElement, CarbohydrateLink, Carbohydrates, CarbohydrateTerminalLink, PartialCarbohydrateElement, EmptyCarbohydrates } from './data';
 import { UnitRings, UnitRing } from '../unit/rings';
 import { ElementIndex } from '../../model/indexing';
+import { cantorPairing } from '../../../../mol-data/util';
 
 const C = ElementSymbol('C'), O = ElementSymbol('O');
 const SugarRingFps = [
@@ -88,16 +89,14 @@ function getAtomId(unit: Unit.Atomic, index: StructureElement.UnitIndex) {
 function filterFusedRings(unitRings: UnitRings, rings: UnitRings.Index[] | undefined) {
     if (!rings || !rings.length) return
 
+    const { unit, all } = unitRings
     const fusedRings = new Set<UnitRings.Index>()
     const ringCombinations = combinations(fillSerial(new Array(rings.length) as number[]), 2)
     for (let i = 0, il = ringCombinations.length; i < il; ++i) {
         const rc = ringCombinations[i];
-        const r0 = unitRings.all[rings[rc[0]]], r1 = unitRings.all[rings[rc[1]]];
-        if (SortedArray.areIntersecting(r0, r1)) {
-            // TODO: is this a correct check?
-            if (UnitRing.getAltId(unitRings.unit, r0) !== UnitRing.getAltId(unitRings.unit, r1)) {
-                continue;
-            }
+        const r0 = all[rings[rc[0]]], r1 = all[rings[rc[1]]];
+        if (SortedArray.areIntersecting(r0, r1) &&
+                UnitRing.getAltId(unit, r0) === UnitRing.getAltId(unit, r1)) {
             fusedRings.add(rings[rc[0]])
             fusedRings.add(rings[rc[1]])
         }
@@ -128,11 +127,14 @@ export function computeCarbohydrates(structure: Structure): Carbohydrates {
     const elements: CarbohydrateElement[] = []
     const partialElements: PartialCarbohydrateElement[] = []
 
-    const elementsWithRingMap = new Map<string, number>()
-
+    const elementsWithRingMap = new Map<string, number[]>()
     function ringElementKey(residueIndex: number, unitId: number, altId: string) {
         return `${residueIndex}|${unitId}|${altId}`
     }
+    function addRingElement(key: string, elementIndex: number) {
+        if (elementsWithRingMap.has(key)) elementsWithRingMap.get(key)!.push(elementIndex)
+        else elementsWithRingMap.set(key, [elementIndex])
+    }
 
     function fixLinkDirection(iA: number, iB: number) {
         Vec3.sub(elements[iA].geometry.direction, elements[iB].geometry.center, elements[iA].geometry.center)
@@ -151,7 +153,7 @@ export function computeCarbohydrates(structure: Structure): Carbohydrates {
         const unit = structure.units[i]
         if (!Unit.isAtomic(unit)) continue
 
-        const { model } = unit
+        const { model, rings } = unit
         const { chainAtomSegments, residueAtomSegments, residues } = model.atomicHierarchy
         const { label_comp_id } = residues
 
@@ -170,17 +172,16 @@ export function computeCarbohydrates(structure: Structure): Carbohydrates {
                 if (!saccharideComp) continue
 
                 if (!sugarResidueMap) {
-                    sugarResidueMap = UnitRings.byFingerprintAndResidue(unit.rings, SugarRingFps);
+                    sugarResidueMap = UnitRings.byFingerprintAndResidue(rings, SugarRingFps);
                 }
 
-                const sugarRings = filterFusedRings(unit.rings, sugarResidueMap.get(residueIndex));
+                const sugarRings = filterFusedRings(rings, sugarResidueMap.get(residueIndex));
 
                 if (!sugarRings || !sugarRings.length) {
                     partialElements.push({ unit, residueIndex, component: saccharideComp })
                     continue;
                 }
 
-                const rings = unit.rings;
                 const ringElements: number[] = []
 
                 for (let j = 0, jl = sugarRings.length; j < jl; ++j) {
@@ -196,12 +197,16 @@ export function computeCarbohydrates(structure: Structure): Carbohydrates {
                     const ringAltId = UnitRing.getAltId(unit, ringAtoms)
                     const elementIndex = elements.length
                     ringElements.push(elementIndex)
-                    elementsWithRingMap.set(ringElementKey(residueIndex, unit.id, ringAltId), elementIndex)
+
+                    addRingElement(ringElementKey(residueIndex, unit.id, ringAltId), elementIndex)
+                    if (ringAltId) addRingElement(ringElementKey(residueIndex, unit.id, ''), elementIndex)
+
                     elements.push({
                         geometry: { center, normal, direction },
                         component: saccharideComp,
-                        unit, residueIndex, anomericCarbon, ringAltId,
-                        ringMemberCount: ringAtoms.length
+                        ringIndex: sugarRings[j],
+                        altId: ringAltId,
+                        unit, residueIndex
                     })
                 }
 
@@ -215,9 +220,9 @@ export function computeCarbohydrates(structure: Structure): Carbohydrates {
                     if (IntAdjacencyGraph.areVertexSetsConnected(unit.bonds, r0, r1, 3)) {
                         const re0 = ringElements[rc[0]]
                         const re1 = ringElements[rc[1]]
-                        if (elements[re0].ringAltId === elements[re1].ringAltId) {
+                        if (elements[re0].altId === elements[re1].altId) {
                             // TODO handle better, for now fix both directions as it is unclear where the C1 atom is
-                            //  would need to know the path connecting the two rings
+                            //      would need to know the path connecting the two rings
                             fixLinkDirection(re0, re1)
                             fixLinkDirection(re1, re0)
                             links.push({ carbohydrateIndexA: re0, carbohydrateIndexB: re1 })
@@ -229,80 +234,86 @@ export function computeCarbohydrates(structure: Structure): Carbohydrates {
         }
     }
 
-    function getRingElementIndex(unit: Unit.Atomic, index: StructureElement.UnitIndex) {
-        return elementsWithRingMap.get(ringElementKey(unit.getResidueIndex(index), unit.id, getAltId(unit, index)))
+    function getRingElementIndices(unit: Unit.Atomic, index: StructureElement.UnitIndex) {
+        return elementsWithRingMap.get(ringElementKey(unit.getResidueIndex(index), unit.id, getAltId(unit, index))) || []
     }
 
     // add carbohydrate links induced by intra-unit bonds
     // (e.g. for structures from the PDB archive __after__ carbohydrate remediation)
     for (let i = 0, il = elements.length; i < il; ++i) {
-        const carbohydrate = elements[i]
-        const { unit, residueIndex, anomericCarbon } = carbohydrate
-        const { offset, b } = unit.bonds
-        const ac = SortedArray.indexOf(unit.elements, anomericCarbon) as StructureElement.UnitIndex
-
-        for (let j = offset[ac], jl = offset[ac + 1]; j < jl; ++j) {
-            const bj = b[j] as StructureElement.UnitIndex
-            if (residueIndex !== unit.getResidueIndex(bj)) {
-                const ringElementIndex = getRingElementIndex(unit, bj)
-                if (ringElementIndex !== undefined && ringElementIndex !== i) {
-                    fixLinkDirection(i, ringElementIndex)
-                    links.push({
-                        carbohydrateIndexA: i,
-                        carbohydrateIndexB: ringElementIndex
-                    })
-                    links.push({
-                        carbohydrateIndexA: ringElementIndex,
-                        carbohydrateIndexB: i
-                    })
-                }
+        const cA = elements[i]
+        const { unit } = cA
+
+        for (let j = i + 1; j < il; ++j) {
+            const cB = elements[j]
+            if (unit !== cB.unit || cA.residueIndex === cB.residueIndex) continue
+            const rA = unit.rings.all[cA.ringIndex]
+            const rB = unit.rings.all[cB.ringIndex]
+
+            if (IntAdjacencyGraph.areVertexSetsConnected(unit.bonds, rA, rB, 3)) {
+                // TODO handle better, for now fix both directions as it is unclear where the C1 atom is
+                //      would need to know the path connecting the two rings
+                fixLinkDirection(i, j)
+                fixLinkDirection(j, i)
+                links.push({ carbohydrateIndexA: i, carbohydrateIndexB: j })
+                links.push({ carbohydrateIndexA: j, carbohydrateIndexB: i })
             }
         }
-
     }
 
     // get carbohydrate links induced by inter-unit bonds, that is
-    // terminal links plus inter monosaccharide links for structures from the
+    // inter monosaccharide links for structures from the
     // PDB archive __before__ carbohydrate remediation
+    // plus terminal links for __before__ and __after__
     for (let i = 0, il = structure.units.length; i < il; ++i) {
         const unit = structure.units[i]
         if (!Unit.isAtomic(unit)) continue
 
         structure.interUnitBonds.getConnectedUnits(unit).forEach(pairBonds => {
             pairBonds.connectedIndices.forEach(indexA => {
-                pairBonds.getEdges(indexA).forEach(bondInfo => {
+                pairBonds.getEdges(indexA).forEach(({ props, indexB }) => {
+                    if (!BondType.isCovalent(props.flag)) return
+
                     const { unitA, unitB } = pairBonds
-                    const indexB = bondInfo.indexB
-                    const ringElementIndexA = getRingElementIndex(unitA, indexA)
-                    const ringElementIndexB = getRingElementIndex(unitB, indexB)
-
-                    if (ringElementIndexA !== undefined && ringElementIndexB !== undefined) {
-                        const atomIdA = getAtomId(unitA, indexA)
-                        if (atomIdA.startsWith('O1') || atomIdA.startsWith('C1')) {
-                            fixLinkDirection(ringElementIndexA, ringElementIndexB)
+                    const ringElementIndicesA = getRingElementIndices(unitA, indexA)
+                    const ringElementIndicesB = getRingElementIndices(unitB, indexB)
+                    if (ringElementIndicesA.length > 0 && ringElementIndicesB.length > 0) {
+                        const lA = ringElementIndicesA.length
+                        const lB = ringElementIndicesB.length
+                        for (let j = 0, jl = Math.max(lA, lB); j < jl; ++j) {
+                            const ringElementIndexA = ringElementIndicesA[Math.min(j, lA - 1)]
+                            const ringElementIndexB = ringElementIndicesB[Math.min(j, lB - 1)]
+                            const atomIdA = getAtomId(unitA, indexA)
+                            if (atomIdA.startsWith('O1') || atomIdA.startsWith('C1')) {
+                                fixLinkDirection(ringElementIndexA, ringElementIndexB)
+                            }
+                            links.push({
+                                carbohydrateIndexA: ringElementIndexA,
+                                carbohydrateIndexB: ringElementIndexB
+                            })
+                        }
+                    } else if (ringElementIndicesB.length === 0) {
+                        for (const ringElementIndexA of ringElementIndicesA) {
+                            const atomIdA = getAtomId(unitA, indexA)
+                            if (atomIdA.startsWith('O1') || atomIdA.startsWith('C1')) {
+                                fixTerminalLinkDirection(ringElementIndexA, indexB, unitB)
+                            }
+                            terminalLinks.push({
+                                carbohydrateIndex: ringElementIndexA,
+                                elementIndex: indexB,
+                                elementUnit: unitB,
+                                fromCarbohydrate: true
+                            })
                         }
-                        links.push({
-                            carbohydrateIndexA: ringElementIndexA,
-                            carbohydrateIndexB: ringElementIndexB
-                        })
-                    } else if (ringElementIndexA !== undefined) {
-                        const atomIdA = getAtomId(unitA, indexA)
-                        if (atomIdA.startsWith('O1') || atomIdA.startsWith('C1')) {
-                            fixTerminalLinkDirection(ringElementIndexA, indexB, unitB)
+                    } else if (ringElementIndicesA.length === 0) {
+                        for (const ringElementIndexB of ringElementIndicesB) {
+                            terminalLinks.push({
+                                carbohydrateIndex: ringElementIndexB,
+                                elementIndex: indexA,
+                                elementUnit: unitA,
+                                fromCarbohydrate: false
+                            })
                         }
-                        terminalLinks.push({
-                            carbohydrateIndex: ringElementIndexA,
-                            elementIndex: indexB,
-                            elementUnit: unitB,
-                            fromCarbohydrate: true
-                        })
-                    } else if (ringElementIndexB !== undefined) {
-                        terminalLinks.push({
-                            carbohydrateIndex: ringElementIndexB,
-                            elementIndex: indexA,
-                            elementUnit: unitA,
-                            fromCarbohydrate: false
-                        })
                     }
                 })
             })
@@ -313,128 +324,82 @@ export function computeCarbohydrates(structure: Structure): Carbohydrates {
 }
 
 function buildLookups (elements: CarbohydrateElement[], links: CarbohydrateLink[], terminalLinks: CarbohydrateTerminalLink[]) {
-    // element lookup
-
-    function elementKey(unit: Unit, anomericCarbon: ElementIndex) {
-        return `${unit.id}|${anomericCarbon}`
-    }
 
-    const elementMap = new Map<string, number>()
-    for (let i = 0, il = elements.length; i < il; ++i) {
-        const { unit, anomericCarbon } = elements[i]
-        elementMap.set(elementKey(unit, anomericCarbon), i)
+    function key(unit: Unit, element: ElementIndex) {
+        return cantorPairing(unit.id, element)
     }
 
-    function getElementIndex(unit: Unit, anomericCarbon: ElementIndex) {
-        return elementMap.get(elementKey(unit, anomericCarbon))
+    function getIndices(map: Map<number, number[]>, unit: Unit.Atomic, index: ElementIndex): ReadonlyArray<number> {
+        const indices: number[] = []
+        const il = map.get(key(unit, index))
+        if (il !== undefined) {
+            for (const i of il) arraySetAdd(indices, i)
+        }
+        return indices
     }
 
-    // link lookup
+    // elements
 
-    function linkKey(unitA: Unit, anomericCarbonA: ElementIndex, unitB: Unit, anomericCarbonB: ElementIndex) {
-        return `${unitA.id}|${anomericCarbonA}|${unitB.id}|${anomericCarbonB}`
-    }
-
-    const linkMap = new Map<string, number>()
-    for (let i = 0, il = links.length; i < il; ++i) {
-        const l = links[i]
-        const { unit: unitA, anomericCarbon: anomericCarbonA } = elements[l.carbohydrateIndexA]
-        const { unit: unitB, anomericCarbon: anomericCarbonB } = elements[l.carbohydrateIndexB]
-        linkMap.set(linkKey(unitA, anomericCarbonA, unitB, anomericCarbonB), i)
+    const elementsMap = new Map<number, number[]>()
+    for (let i = 0, il = elements.length; i < il; ++i) {
+        const { unit, ringIndex } = elements[i]
+        const ring = unit.rings.all[ringIndex]
+        for (let j = 0, jl = ring.length; j < jl; ++j) {
+            const k = key(unit, unit.elements[ring[j]])
+            const e = elementsMap.get(k)
+            if (e === undefined) elementsMap.set(k, [i])
+            else e.push(i)
+        }
     }
 
-    function getLinkIndex(unitA: Unit, anomericCarbonA: ElementIndex, unitB: Unit, anomericCarbonB: ElementIndex) {
-        return linkMap.get(linkKey(unitA, anomericCarbonA, unitB, anomericCarbonB))
+    function getElementIndices(unit: Unit.Atomic, index: ElementIndex) {
+        return getIndices(elementsMap, unit, index)
     }
 
-    // links lookup
+    // links
 
-    function linksKey(unit: Unit, anomericCarbon: ElementIndex) {
-        return `${unit.id}|${anomericCarbon}`
-    }
-
-    const linksMap = new Map<string, number[]>()
+    const linksMap = new Map<number, number[]>()
     for (let i = 0, il = links.length; i < il; ++i) {
         const l = links[i]
-        const { unit, anomericCarbon } = elements[l.carbohydrateIndexA]
-        const k = linksKey(unit, anomericCarbon)
-        const e = linksMap.get(k)
-        if (e === undefined) linksMap.set(k, [i])
-        else e.push(i)
-    }
-
-    function getLinkIndices(unit: Unit, anomericCarbon: ElementIndex): ReadonlyArray<number> {
-        return linksMap.get(linksKey(unit, anomericCarbon)) || []
-    }
-
-    // terminal link lookup
-
-    function terminalLinkKey(unitA: Unit, elementA: ElementIndex, unitB: Unit, elementB: ElementIndex) {
-        return `${unitA.id}|${elementA}|${unitB.id}|${elementB}`
-    }
-
-    const terminalLinkMap = new Map<string, number>()
-    for (let i = 0, il = terminalLinks.length; i < il; ++i) {
-        const { fromCarbohydrate, carbohydrateIndex, elementUnit, elementIndex } = terminalLinks[i]
-        const { unit, anomericCarbon } = elements[carbohydrateIndex]
-        if (fromCarbohydrate) {
-            terminalLinkMap.set(terminalLinkKey(unit, anomericCarbon, elementUnit, elementUnit.elements[elementIndex]), i)
-        } else {
-            terminalLinkMap.set(terminalLinkKey(elementUnit, elementUnit.elements[elementIndex], unit, anomericCarbon), i)
+        const { unit, ringIndex } = elements[l.carbohydrateIndexA]
+        const ring = unit.rings.all[ringIndex]
+        for (let j = 0, jl = ring.length; j < jl; ++j) {
+            const k = key(unit, unit.elements[ring[j]])
+            const e = linksMap.get(k)
+            if (e === undefined) linksMap.set(k, [i])
+            else e.push(i)
         }
     }
 
-    function getTerminalLinkIndex(unitA: Unit, elementA: ElementIndex, unitB: Unit, elementB: ElementIndex) {
-        return terminalLinkMap.get(terminalLinkKey(unitA, elementA, unitB, elementB))
+    function getLinkIndices(unit: Unit.Atomic, index: ElementIndex) {
+        return getIndices(linksMap, unit, index)
     }
 
-    // terminal links lookup
-
-    function terminalLinksKey(unit: Unit, element: ElementIndex) {
-        return `${unit.id}|${element}`
-    }
+    // terminal links
 
-    const terminalLinksMap = new Map<string, number[]>()
+    const terminalLinksMap = new Map<number, number[]>()
     for (let i = 0, il = terminalLinks.length; i < il; ++i) {
         const { fromCarbohydrate, carbohydrateIndex, elementUnit, elementIndex } = terminalLinks[i]
-        const { unit, anomericCarbon } = elements[carbohydrateIndex]
-        let k: string
         if (fromCarbohydrate) {
-            k = terminalLinksKey(unit, anomericCarbon)
-        } else {
-            k = terminalLinksKey(elementUnit, elementUnit.elements[elementIndex])
-        }
-        const e = terminalLinksMap.get(k)
-        if (e === undefined) terminalLinksMap.set(k, [i])
-        else e.push(i)
-    }
-
-    function getTerminalLinkIndices(unit: Unit, element: ElementIndex): ReadonlyArray<number> {
-        return terminalLinksMap.get(terminalLinksKey(unit, element)) || []
-    }
-
-    // anomeric carbon lookup
-
-    function anomericCarbonKey(unit: Unit, residueIndex: ResidueIndex) {
-        return `${unit.id}|${residueIndex}`
-    }
-
-    const anomericCarbonMap = new Map<string, ElementIndex[]>()
-    for (let i = 0, il = elements.length; i < il; ++i) {
-        const { unit, anomericCarbon } = elements[i]
-        const residueIndex = unit.model.atomicHierarchy.residueAtomSegments.index[anomericCarbon]
-        const k = anomericCarbonKey(unit, residueIndex)
-        if (anomericCarbonMap.has(k)) {
-            anomericCarbonMap.get(k)!.push(anomericCarbon)
+            const { unit, ringIndex } = elements[carbohydrateIndex]
+            const ring = unit.rings.all[ringIndex]
+            for (let j = 0, jl = ring.length; j < jl; ++j) {
+                const k = key(unit, unit.elements[ring[j]])
+                const e = terminalLinksMap.get(k)
+                if (e === undefined) terminalLinksMap.set(k, [i])
+                else e.push(i)
+            }
         } else {
-            anomericCarbonMap.set(k, [anomericCarbon])
+            const k = key(elementUnit, elementUnit.elements[elementIndex])
+            const e = terminalLinksMap.get(k)
+            if (e === undefined) terminalLinksMap.set(k, [i])
+            else e.push(i)
         }
     }
 
-    const EmptyArray: ReadonlyArray<any> = []
-    function getAnomericCarbons(unit: Unit, residueIndex: ResidueIndex) {
-        return anomericCarbonMap.get(anomericCarbonKey(unit, residueIndex)) || EmptyArray
+    function getTerminalLinkIndices(unit: Unit.Atomic, index: ElementIndex) {
+        return getIndices(terminalLinksMap, unit, index)
     }
 
-    return { getElementIndex, getLinkIndex, getLinkIndices, getTerminalLinkIndex, getTerminalLinkIndices, getAnomericCarbons }
+    return { getElementIndices, getLinkIndices, getTerminalLinkIndices }
 }

+ 11 - 15
src/mol-model/structure/structure/carbohydrates/data.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -9,6 +9,7 @@ import { Vec3 } from '../../../../mol-math/linear-algebra';
 import { ResidueIndex, ElementIndex } from '../../model';
 import { SaccharideComponent } from './constants';
 import StructureElement from '../element';
+import { UnitRings } from '../unit/rings';
 
 export interface CarbohydrateLink {
     readonly carbohydrateIndexA: number
@@ -18,19 +19,18 @@ export interface CarbohydrateLink {
 export interface CarbohydrateTerminalLink {
     readonly carbohydrateIndex: number
     readonly elementIndex: StructureElement.UnitIndex
-    readonly elementUnit: Unit
+    readonly elementUnit: Unit.Atomic
     /** specifies direction of the link */
     readonly fromCarbohydrate: boolean
 }
 
 export interface CarbohydrateElement {
     readonly geometry: { readonly center: Vec3, readonly normal: Vec3, readonly direction: Vec3 },
-    readonly anomericCarbon: ElementIndex,
     readonly unit: Unit.Atomic,
     readonly residueIndex: ResidueIndex,
     readonly component: SaccharideComponent,
-    readonly ringAltId: string,
-    readonly ringMemberCount: number,
+    readonly ringIndex: UnitRings.Index,
+    readonly altId: string
 }
 
 /** partial carbohydrate with no ring present */
@@ -45,12 +45,10 @@ export interface Carbohydrates {
     terminalLinks: ReadonlyArray<CarbohydrateTerminalLink>
     elements: ReadonlyArray<CarbohydrateElement>
     partialElements: ReadonlyArray<PartialCarbohydrateElement>
-    getElementIndex: (unit: Unit, anomericCarbon: ElementIndex) => number | undefined
-    getLinkIndex: (unitA: Unit, anomericCarbonA: ElementIndex, unitB: Unit, anomericCarbonB: ElementIndex) => number | undefined
-    getLinkIndices: (unit: Unit, anomericCarbon: ElementIndex) => ReadonlyArray<number>
-    getTerminalLinkIndex: (unitA: Unit, elementA: ElementIndex, unitB: Unit, elementB: ElementIndex) => number | undefined
-    getTerminalLinkIndices: (unit: Unit, element: ElementIndex) => ReadonlyArray<number>
-    getAnomericCarbons: (unit: Unit, residueIndex: ResidueIndex) => ReadonlyArray<ElementIndex>
+
+    getElementIndices: (unit: Unit.Atomic, element: ElementIndex) => ReadonlyArray<number>
+    getLinkIndices: (unit: Unit.Atomic, element: ElementIndex) => ReadonlyArray<number>
+    getTerminalLinkIndices: (unit: Unit.Atomic, element: ElementIndex) => ReadonlyArray<number>
 }
 
 const EmptyArray: ReadonlyArray<any> = []
@@ -59,10 +57,8 @@ export const EmptyCarbohydrates: Carbohydrates = {
     terminalLinks: EmptyArray,
     elements: EmptyArray,
     partialElements: EmptyArray,
-    getElementIndex: () => undefined,
-    getLinkIndex: () => undefined,
+
+    getElementIndices: () => EmptyArray,
     getLinkIndices: () => EmptyArray,
-    getTerminalLinkIndex: () => undefined,
     getTerminalLinkIndices: () => EmptyArray,
-    getAnomericCarbons: () => [],
 }

+ 27 - 49
src/mol-repr/structure/visual/carbohydrate-link-cylinder.ts

@@ -4,7 +4,7 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Structure, Bond, StructureElement } from '../../../mol-model/structure';
+import { Structure, StructureElement, Unit } from '../../../mol-model/structure';
 import { Loci, EmptyLoci } from '../../../mol-model/loci';
 import { Vec3 } from '../../../mol-math/linear-algebra';
 import { createLinkCylinderMesh, LinkCylinderParams } from './util/link';
@@ -18,6 +18,7 @@ import { PickingId } from '../../../mol-geo/geometry/picking';
 import { VisualUpdateState } from '../../util';
 import { VisualContext } from '../../../mol-repr/visual';
 import { Theme } from '../../../mol-theme/theme';
+import { getAltResidueLociFromId } from './util/common';
 
 function createCarbohydrateLinkCylinderMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<CarbohydrateLinkParams>, mesh?: Mesh) {
     const { links, elements } = structure.carbohydrates
@@ -34,8 +35,10 @@ function createCarbohydrateLinkCylinderMesh(ctx: VisualContext, structure: Struc
         },
         radius: (edgeIndex: number) => {
             const l = links[edgeIndex]
-            location.unit = elements[l.carbohydrateIndexA].unit
-            location.element = elements[l.carbohydrateIndexA].anomericCarbon
+            const carbA = elements[l.carbohydrateIndexA]
+            const ringA = carbA.unit.rings.all[carbA.ringIndex]
+            location.unit = carbA.unit
+            location.element = carbA.unit.elements[ringA[0]]
             return theme.size.size(location) * linkSizeFactor
         },
     }
@@ -71,17 +74,13 @@ function CarbohydrateLinkIterator(structure: Structure): LocationIterator {
     const { elements, links } = structure.carbohydrates
     const groupCount = links.length
     const instanceCount = 1
-    const location = Bond.Location()
+    const location = StructureElement.Location.create()
     const getLocation = (groupIndex: number) => {
         const link = links[groupIndex]
         const carbA = elements[link.carbohydrateIndexA]
-        const carbB = elements[link.carbohydrateIndexB]
-        const indexA = OrderedSet.indexOf(carbA.unit.elements, carbA.anomericCarbon)
-        const indexB = OrderedSet.indexOf(carbB.unit.elements, carbB.anomericCarbon)
-        location.aUnit = carbA.unit
-        location.aIndex = indexA as StructureElement.UnitIndex
-        location.bUnit = carbB.unit
-        location.bIndex = indexB as StructureElement.UnitIndex
+        const ringA = carbA.unit.rings.all[carbA.ringIndex]
+        location.unit = carbA.unit
+        location.element = carbA.unit.elements[ringA[0]]
         return location
     }
     return LocationIterator(groupCount, instanceCount, getLocation, true)
@@ -94,51 +93,30 @@ function getLinkLoci(pickingId: PickingId, structure: Structure, id: number) {
         const l = links[groupId]
         const carbA = elements[l.carbohydrateIndexA]
         const carbB = elements[l.carbohydrateIndexB]
-        const indexA = OrderedSet.indexOf(carbA.unit.elements, carbA.anomericCarbon)
-        const indexB = OrderedSet.indexOf(carbB.unit.elements, carbB.anomericCarbon)
-        if (indexA !== -1 && indexB !== -1) {
-            return Bond.Loci(structure, [
-                Bond.Location(
-                    carbA.unit, indexA as StructureElement.UnitIndex,
-                    carbB.unit, indexB as StructureElement.UnitIndex
-                ),
-                Bond.Location(
-                    carbB.unit, indexB as StructureElement.UnitIndex,
-                    carbA.unit, indexA as StructureElement.UnitIndex
-                )
-            ])
-        }
+        return StructureElement.Loci.union(
+            getAltResidueLociFromId(structure, carbA.unit, carbA.residueIndex, carbA.altId),
+            getAltResidueLociFromId(structure, carbB.unit, carbB.residueIndex, carbB.altId)
+        )
     }
     return EmptyLoci
 }
 
 function eachCarbohydrateLink(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) {
     let changed = false
-    if (Bond.isLoci(loci)) {
-        if (!Structure.areEquivalent(loci.structure, structure)) return false
-        const { getLinkIndex } = structure.carbohydrates
-        for (const l of loci.bonds) {
-            const idx = getLinkIndex(l.aUnit, l.aUnit.elements[l.aIndex], l.bUnit, l.bUnit.elements[l.bIndex])
-            if (idx !== undefined) {
-                if (apply(Interval.ofSingleton(idx))) changed = true
+    if (!StructureElement.Loci.is(loci)) return false
+    if (!Structure.areEquivalent(loci.structure, structure)) return false
+
+    const { getLinkIndices } = structure.carbohydrates
+    for (const { unit, indices } of loci.elements) {
+        if (!Unit.isAtomic(unit)) continue
+
+        OrderedSet.forEach(indices, v => {
+            // TODO avoid duplicate calls to apply
+            const linkIndices = getLinkIndices(unit, unit.elements[v])
+            for (let i = 0, il = linkIndices.length; i < il; ++i) {
+                if (apply(Interval.ofSingleton(linkIndices[i]))) changed = true
             }
-        }
-    } else if (StructureElement.Loci.is(loci)) {
-        if (!Structure.areEquivalent(loci.structure, structure)) return false
-        // TODO mark link only when both of the link elements are in a StructureElement.Loci
-        const { getElementIndex, getLinkIndices, elements } = structure.carbohydrates
-        for (const e of loci.elements) {
-            OrderedSet.forEach(e.indices, v => {
-                const carbI = getElementIndex(e.unit, e.unit.elements[v])
-                if (carbI !== undefined) {
-                    const carb = elements[carbI]
-                    const indices = getLinkIndices(carb.unit, carb.anomericCarbon)
-                    for (let i = 0, il = indices.length; i < il; ++i) {
-                        if (apply(Interval.ofSingleton(indices[i]))) changed = true
-                    }
-                }
-            })
-        }
+        })
     }
     return changed
 }

+ 19 - 23
src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -10,7 +10,7 @@ import { OctagonalPyramid, PerforatedOctagonalPyramid } from '../../../mol-geo/p
 import { Star } from '../../../mol-geo/primitive/star';
 import { Octahedron, PerforatedOctahedron } from '../../../mol-geo/primitive/octahedron';
 import { DiamondPrism, PentagonalPrism, ShiftedHexagonalPrism, HexagonalPrism, HeptagonalPrism } from '../../../mol-geo/primitive/prism';
-import { Structure, StructureElement } from '../../../mol-model/structure';
+import { Structure, StructureElement, Unit } from '../../../mol-model/structure';
 import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
 import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder';
 import { getSaccharideShape, SaccharideShape } from '../../../mol-model/structure/structure/carbohydrates/constants';
@@ -25,7 +25,7 @@ import { OrderedSet, Interval } from '../../../mol-data/int';
 import { EmptyLoci, Loci } from '../../../mol-model/loci';
 import { VisualContext } from '../../../mol-repr/visual';
 import { Theme } from '../../../mol-theme/theme';
-import { getAltResidueLoci } from './util/common';
+import { getAltResidueLociFromId } from './util/common';
 
 const t = Mat4.identity()
 const sVec = Vec3.zero()
@@ -57,10 +57,11 @@ function createCarbohydrateSymbolMesh(ctx: VisualContext, structure: Structure,
 
     for (let i = 0; i < n; ++i) {
         const c = carbohydrates.elements[i];
-        const shapeType = getSaccharideShape(c.component.type, c.ringMemberCount)
+        const ring = c.unit.rings.all[c.ringIndex]
+        const shapeType = getSaccharideShape(c.component.type, ring.length)
 
         l.unit = c.unit
-        l.element = c.unit.elements[c.anomericCarbon]
+        l.element = c.unit.elements[ring[0]]
         const size = theme.size.size(l)
         const radius = size * sizeFactor
         const side = size * sizeFactor * SideFactor
@@ -189,8 +190,9 @@ function CarbohydrateElementIterator(structure: Structure): LocationIterator {
     const location = StructureElement.Location.create()
     function getLocation (groupIndex: number, instanceIndex: number) {
         const carb = carbElements[Math.floor(groupIndex / 2)]
+        const ring = carb.unit.rings.all[carb.ringIndex]
         location.unit = carb.unit
-        location.element = carb.anomericCarbon
+        location.element = carb.unit.elements[ring[0]]
         return location
     }
     function isSecondary (elementIndex: number, instanceIndex: number) {
@@ -204,32 +206,26 @@ function getCarbohydrateLoci(pickingId: PickingId, structure: Structure, id: num
     const { objectId, groupId } = pickingId
     if (id === objectId) {
         const carb = structure.carbohydrates.elements[Math.floor(groupId / 2)]
-        return getAltResidueLoci(structure, carb.unit, carb.anomericCarbon)
+        return getAltResidueLociFromId(structure, carb.unit, carb.residueIndex, carb.altId)
     }
     return EmptyLoci
 }
 
 /** For each carbohydrate (usually a monosaccharide) when all its residue's elements are in a loci. */
 function eachCarbohydrate(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) {
-    const { getElementIndex, getAnomericCarbons } = structure.carbohydrates
+    const { getElementIndices } = structure.carbohydrates
     let changed = false
     if (!StructureElement.Loci.is(loci)) return false
     if (!Structure.areEquivalent(loci.structure, structure)) return false
-    for (const e of loci.elements) {
-        // TODO make more efficient by handling/grouping `e.indices` by residue index
-        // TODO only call apply when the full alt-residue of the unit is part of `e`
-        OrderedSet.forEach(e.indices, v => {
-            const { model, elements } = e.unit
-            const { index } = model.atomicHierarchy.residueAtomSegments
-            const rI = index[elements[v]]
-            const eIndices = getAnomericCarbons(e.unit, rI)
-            for (let i = 0, il = eIndices.length; i < il; ++i) {
-                const eI = eIndices[i]
-                if (!OrderedSet.has(e.indices, OrderedSet.indexOf(elements, eI))) continue
-                const idx = getElementIndex(e.unit, eI)
-                if (idx !== undefined) {
-                    if (apply(Interval.ofBounds(idx * 2, idx * 2 + 2))) changed = true
-                }
+
+    for (const { unit, indices } of loci.elements) {
+        if (!Unit.isAtomic(unit)) continue
+
+        OrderedSet.forEach(indices, v => {
+            // TODO avoid duplicate calls to apply
+            const elementIndices = getElementIndices(unit, unit.elements[v])
+            for (let i = 0, il = elementIndices.length; i < il; ++i) {
+                if (apply(Interval.ofSingleton(elementIndices[i] * 2))) changed = true
             }
         })
     }

+ 30 - 54
src/mol-repr/structure/visual/carbohydrate-terminal-link-cylinder.ts

@@ -6,7 +6,7 @@
 
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
 import { VisualContext } from '../../visual';
-import { Structure, StructureElement, Bond } from '../../../mol-model/structure';
+import { Structure, StructureElement, Unit } 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';
@@ -19,6 +19,7 @@ import { OrderedSet, Interval } from '../../../mol-data/int';
 import { PickingId } from '../../../mol-geo/geometry/picking';
 import { EmptyLoci, Loci } from '../../../mol-model/loci';
 import { getElementIdx, MetalsSet } from '../../../mol-model/structure/structure/unit/bonds/common';
+import { getAltResidueLociFromId, getAltResidueLoci } from './util/common';
 
 function createCarbohydrateTerminalLinkCylinderMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<CarbohydrateTerminalLinkParams>, mesh?: Mesh) {
     const { terminalLinks, elements } = structure.carbohydrates
@@ -41,8 +42,10 @@ function createCarbohydrateTerminalLinkCylinderMesh(ctx: VisualContext, structur
         radius: (edgeIndex: number) => {
             const l = terminalLinks[edgeIndex]
             if (l.fromCarbohydrate) {
-                location.unit = elements[l.carbohydrateIndex].unit
-                location.element = elements[l.carbohydrateIndex].anomericCarbon
+                const carb = elements[l.carbohydrateIndex]
+                const ring = carb.unit.rings.all[carb.ringIndex]
+                location.unit = carb.unit
+                location.element = carb.unit.elements[ring[0]]
             } else {
                 location.unit = l.elementUnit
                 location.element = l.elementUnit.elements[l.elementIndex]
@@ -88,21 +91,17 @@ function CarbohydrateTerminalLinkIterator(structure: Structure): LocationIterato
     const { elements, terminalLinks } = structure.carbohydrates
     const groupCount = terminalLinks.length
     const instanceCount = 1
-    const location = Bond.Location()
+    const location = StructureElement.Location.create()
     const getLocation = (groupIndex: number) => {
         const terminalLink = terminalLinks[groupIndex]
-        const carb = elements[terminalLink.carbohydrateIndex]
-        const indexCarb = OrderedSet.indexOf(carb.unit.elements, carb.anomericCarbon)
         if (terminalLink.fromCarbohydrate) {
-            location.aUnit = carb.unit
-            location.aIndex = indexCarb as StructureElement.UnitIndex
-            location.bUnit = terminalLink.elementUnit
-            location.bIndex = terminalLink.elementIndex
+            const carb = elements[terminalLink.carbohydrateIndex]
+            const ring = carb.unit.rings.all[carb.ringIndex]
+            location.unit = carb.unit
+            location.element = carb.unit.elements[ring[0]]
         } else {
-            location.aUnit = terminalLink.elementUnit
-            location.aIndex = terminalLink.elementIndex
-            location.bUnit = carb.unit
-            location.bIndex = indexCarb as StructureElement.UnitIndex
+            location.unit = terminalLink.elementUnit
+            location.element = terminalLink.elementUnit.elements[terminalLink.elementIndex]
         }
         return location
     }
@@ -115,54 +114,31 @@ function getTerminalLinkLoci(pickingId: PickingId, structure: Structure, id: num
         const { terminalLinks, elements } = structure.carbohydrates
         const l = terminalLinks[groupId]
         const carb = elements[l.carbohydrateIndex]
-        const carbIndex = OrderedSet.indexOf(carb.unit.elements, carb.anomericCarbon)
 
-        return Bond.Loci(structure, [
-            Bond.Location(
-                carb.unit, carbIndex as StructureElement.UnitIndex,
-                l.elementUnit, l.elementIndex
-            ),
-            Bond.Location(
-                l.elementUnit, l.elementIndex,
-                carb.unit, carbIndex as StructureElement.UnitIndex
-            )
-        ])
+        return StructureElement.Loci.union(
+            getAltResidueLociFromId(structure, carb.unit, carb.residueIndex, carb.altId),
+            getAltResidueLoci(structure, l.elementUnit, l.elementUnit.elements[l.elementIndex])
+        )
     }
     return EmptyLoci
 }
 
 function eachTerminalLink(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) {
-    const { getTerminalLinkIndex } = structure.carbohydrates
     let changed = false
-    if (Bond.isLoci(loci)) {
-        if (!Structure.areEquivalent(loci.structure, structure)) return false
-        for (const l of loci.bonds) {
-            const idx = getTerminalLinkIndex(l.aUnit, l.aUnit.elements[l.aIndex], l.bUnit, l.bUnit.elements[l.bIndex])
-            if (idx !== undefined) {
-                if (apply(Interval.ofSingleton(idx))) changed = true
+    if (!StructureElement.Loci.is(loci)) return false
+    if (!Structure.areEquivalent(loci.structure, structure)) return false
+
+    const { getTerminalLinkIndices } = structure.carbohydrates
+    for (const { unit, indices } of loci.elements) {
+        if (!Unit.isAtomic(unit)) continue
+
+        OrderedSet.forEach(indices, v => {
+            // TODO avoid duplicate calls to apply
+            const linkIndices = getTerminalLinkIndices(unit, unit.elements[v])
+            for (let i = 0, il = linkIndices.length; i < il; ++i) {
+                if (apply(Interval.ofSingleton(linkIndices[i]))) changed = true
             }
-        }
-    } else if (StructureElement.Loci.is(loci)) {
-        if (!Structure.areEquivalent(loci.structure, structure)) return false
-        // TODO mark link only when both of the link elements are in a StructureElement.Loci
-        const { getElementIndex, getTerminalLinkIndices, elements } = structure.carbohydrates
-        for (const e of loci.elements) {
-            OrderedSet.forEach(e.indices, v => {
-                const carbI = getElementIndex(e.unit, e.unit.elements[v])
-                if (carbI !== undefined) {
-                    const carb = elements[carbI]
-                    const indices = getTerminalLinkIndices(carb.unit, carb.anomericCarbon)
-                    for (let i = 0, il = indices.length; i < il; ++i) {
-                        if (apply(Interval.ofSingleton(indices[i]))) changed = true
-                    }
-                } else {
-                    const indices = getTerminalLinkIndices(e.unit, e.unit.elements[v])
-                    for (let i = 0, il = indices.length; i < il; ++i) {
-                        if (apply(Interval.ofSingleton(indices[i]))) changed = true
-                    }
-                }
-            })
-        }
+        })
     }
     return changed
 }

+ 22 - 14
src/mol-repr/structure/visual/util/common.ts

@@ -4,7 +4,7 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Unit, Structure, ElementIndex, StructureElement } from '../../../../mol-model/structure';
+import { Unit, Structure, ElementIndex, StructureElement, ResidueIndex } from '../../../../mol-model/structure';
 import { Mat4 } from '../../../../mol-math/linear-algebra';
 import { TransformData, createTransform } from '../../../../mol-geo/geometry/transform-data';
 import { OrderedSet, SortedArray } from '../../../../mol-data/int';
@@ -34,27 +34,35 @@ export function getResidueLoci(structure: Structure, unit: Unit.Atomic, elementI
  * Return a Loci for the elements of a whole residue the elementIndex belongs to but
  * restrict to elements that have the same label_alt_id or none
  */
-export function getAltResidueLoci(structure: Structure, unit: Unit.Atomic, elementIndex: ElementIndex): Loci {
+export function getAltResidueLoci(structure: Structure, unit: Unit.Atomic, elementIndex: ElementIndex) {
     const { elements, model } = unit
     const { label_alt_id } = model.atomicHierarchy.atoms
     const elementAltId = label_alt_id.value(elementIndex)
     if (OrderedSet.indexOf(elements, elementIndex) !== -1) {
-        const { index, offsets } = model.atomicHierarchy.residueAtomSegments
+        const { index } = model.atomicHierarchy.residueAtomSegments
         const rI = index[elementIndex]
-        const _indices: number[] = []
-        for (let i = offsets[rI], il = offsets[rI + 1]; i < il; ++i) {
-            const unitIndex = OrderedSet.indexOf(elements, i)
-            if (unitIndex !== -1) {
-                const altId = label_alt_id.value(i)
-                if (elementAltId === altId || altId === '') {
-                    _indices.push(unitIndex)
-                }
+        return getAltResidueLociFromId(structure, unit, rI, elementAltId)
+    }
+    return StructureElement.Loci(structure, [])
+}
+
+export function getAltResidueLociFromId(structure: Structure, unit: Unit.Atomic, residueIndex: ResidueIndex, elementAltId: string) {
+    const { elements, model } = unit
+    const { label_alt_id } = model.atomicHierarchy.atoms
+    const { offsets } = model.atomicHierarchy.residueAtomSegments
+
+    const _indices: number[] = []
+    for (let i = offsets[residueIndex], il = offsets[residueIndex + 1]; i < il; ++i) {
+        const unitIndex = OrderedSet.indexOf(elements, i)
+        if (unitIndex !== -1) {
+            const altId = label_alt_id.value(i)
+            if (elementAltId === altId || altId === '') {
+                _indices.push(unitIndex)
             }
         }
-        const indices = OrderedSet.ofSortedArray<StructureElement.UnitIndex>(SortedArray.ofSortedArray(_indices))
-        return StructureElement.Loci(structure, [{ unit, indices }])
     }
-    return EmptyLoci
+    const indices = OrderedSet.ofSortedArray<StructureElement.UnitIndex>(SortedArray.ofSortedArray(_indices))
+    return StructureElement.Loci(structure, [{ unit, indices }])
 }
 
 //

+ 1 - 4
src/mol-repr/structure/visual/util/polymer.ts

@@ -170,10 +170,7 @@ export function eachAtomicUnitTracedElement(offset: number, groupSize: number, e
 
 function selectPolymerElements(u: Unit) { return u.polymerElements; }
 
-/**
- * Mark a polymer element (e.g. part of a cartoon trace)
- * - for atomic units mark only when all its residue's elements are in a loci
- */
+/** Mark a polymer element (e.g. part of a cartoon trace) */
 export function eachPolymerElement(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) {
     let changed = false
     if (!StructureElement.Loci.is(loci)) return false

+ 5 - 9
src/mol-theme/color/carbohydrate-symbol.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -26,16 +26,12 @@ export function CarbohydrateSymbolColorTheme(ctx: ThemeDataContext, props: PD.Va
     let color: LocationColor
 
     if (ctx.structure) {
-        const { elements, getElementIndex, getAnomericCarbons } = ctx.structure.carbohydrates
+        const { elements, getElementIndices } = ctx.structure.carbohydrates
 
         const getColor = (unit: Unit, index: ElementIndex) => {
-            const residueIndex = unit.model.atomicHierarchy.residueAtomSegments.index[index]
-            const anomericCarbons = getAnomericCarbons(unit, residueIndex)
-            if (anomericCarbons.length > 0) {
-                const idx = getElementIndex(unit, anomericCarbons[0])
-                if (idx !== undefined) return elements[idx].component.color
-            }
-            return DefaultColor
+            if (!Unit.isAtomic(unit)) return DefaultColor
+            const carbs = getElementIndices(unit, index)
+            return carbs.length > 0 ? elements[carbs[0]].component.color : DefaultColor
         }
 
         color = (location: Location, isSecondary: boolean) => {