Skip to content
Snippets Groups Projects
Commit e4603cae authored by Alexander Rose's avatar Alexander Rose
Browse files

filter fused rings, handle alt locs, anomeric carbon detection

parent 34cbd3fb
Branches
Tags
No related merge requests found
...@@ -286,10 +286,10 @@ export namespace IntAdjacencyGraph { ...@@ -286,10 +286,10 @@ export namespace IntAdjacencyGraph {
* Check if any vertex in `verticesA` is connected to any vertex in `verticesB` * Check if any vertex in `verticesA` is connected to any vertex in `verticesB`
* via at most `maxDistance` edges. * via at most `maxDistance` edges.
* *
* Returns true if A and B are intersecting. * Returns true if verticesA and verticesB are intersecting.
*/ */
export function areVertexSetsConnected(graph: IntAdjacencyGraph, verticesA: SortedArray<number>, verticesB: SortedArray<number>, maxDistance: number): boolean { export function areVertexSetsConnected(graph: IntAdjacencyGraph, verticesA: SortedArray<number>, verticesB: SortedArray<number>, maxDistance: number): boolean {
// check if A and B are intersecting, this handles maxDepth = 0 // check if A and B are intersecting, this handles maxDistance = 0
if (SortedArray.areIntersecting(verticesA, verticesB)) return true; if (SortedArray.areIntersecting(verticesA, verticesB)) return true;
if (maxDistance < 1) return false; if (maxDistance < 1) return false;
......
...@@ -2,9 +2,10 @@ ...@@ -2,9 +2,10 @@
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
* *
* @author Alexander Rose <alexander.rose@weirdbyte.de> * @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author David Sehnal <david.sehnal@gmail.com>
*/ */
import { Segmentation } from 'mol-data/int'; import { Segmentation, SortedArray } from 'mol-data/int';
import { combinations } from 'mol-data/util/combination'; import { combinations } from 'mol-data/util/combination';
import { IntAdjacencyGraph } from 'mol-math/graph'; import { IntAdjacencyGraph } from 'mol-math/graph';
import { Vec3 } from 'mol-math/linear-algebra'; import { Vec3 } from 'mol-math/linear-algebra';
...@@ -19,41 +20,103 @@ import Unit from '../unit'; ...@@ -19,41 +20,103 @@ import Unit from '../unit';
import { SaccharideNameMap, UnknownSaccharideComponent } from './constants'; import { SaccharideNameMap, UnknownSaccharideComponent } from './constants';
import { CarbohydrateElement, CarbohydrateLink, Carbohydrates, CarbohydrateTerminalLink } from './data'; import { CarbohydrateElement, CarbohydrateLink, Carbohydrates, CarbohydrateTerminalLink } from './data';
import { UnitRings, UnitRing } from '../unit/rings'; import { UnitRings, UnitRing } from '../unit/rings';
import { ElementIndex } from '../../model/indexing';
const C = ElementSymbol('C'), O = ElementSymbol('O'); const C = ElementSymbol('C'), O = ElementSymbol('O');
const SugarRingFps = [UnitRing.elementFingerprint([C, C, C, C, C, O]), UnitRing.elementFingerprint([C, C, C, C, O])] const SugarRingFps = [UnitRing.elementFingerprint([C, C, C, C, C, O]), UnitRing.elementFingerprint([C, C, C, C, O])]
function getDirection(direction: Vec3, unit: Unit.Atomic, indices: ArrayLike<StructureElement.UnitIndex>, center: Vec3) { function getAnomericCarbon(unit: Unit.Atomic, ringAtoms: ArrayLike<StructureElement.UnitIndex>): ElementIndex {
let indexC1 = -1, indexC1X = -1, indexC = -1 let indexHasTwoOxygen = -1, indexHasOxygenAndCarbon = -1, indexHasC1Name = -1, indexIsCarbon = -1
const { elements } = unit const { elements } = unit
const { position } = unit.conformation const { type_symbol, label_atom_id } = unit.model.atomicHierarchy.atoms
const { label_atom_id, type_symbol } = unit.model.atomicHierarchy.atoms const { b: neighbor, offset } = unit.links;
for (let i = 0, il = indices.length; i < il; ++i) { for (let i = 0, il = ringAtoms.length; i < il; ++i) {
const ei = elements[indices[i]] const ei = elements[ringAtoms[i]]
const atomId = label_atom_id.value(ei) if (type_symbol.value(ei) !== C) continue
if (atomId === 'C1') { let linkedOxygenCount = 0
indexC1 = ei let linkedCarbonCount = 0
for (let j = offset[ringAtoms[i]], jl = offset[ringAtoms[i] + 1]; j < jl; ++j) {
const ej = elements[neighbor[j]]
const typeSymbol = type_symbol.value(ej)
if (typeSymbol === O) ++linkedOxygenCount
else if (typeSymbol === C) ++linkedCarbonCount
}
if (linkedOxygenCount === 2) {
// found anomeric carbon
indexHasTwoOxygen = ei
break break
} else if (indexC1X === -1 && atomId.startsWith('C1')) { } else if (linkedOxygenCount === 1 && linkedCarbonCount === 1) {
indexC1X = ei // possibly an anomeric carbon if this is a mono-saccharide without a glycosidic bond
} else if (indexC === -1 && type_symbol.value(ei) === C) { indexHasOxygenAndCarbon = ei
indexC = ei } else if (label_atom_id.value(ei).startsWith('C1')) {
// likely the anomeric carbon as it is name C1 by convention
indexHasC1Name = ei
} else {
// use any carbon as a fallback
indexIsCarbon = ei
}
} }
return (indexHasTwoOxygen !== -1 ? indexHasTwoOxygen
: indexHasOxygenAndCarbon !== -1 ? indexHasOxygenAndCarbon
: indexHasC1Name !== -1 ? indexHasC1Name
: indexIsCarbon !== -1 ? indexIsCarbon
: elements[ringAtoms[0]]) as ElementIndex
}
/** Return first non-empty label_alt_id or an empty string */
function getRingAltId(unit: Unit.Atomic, ringAtoms: SortedArray<StructureElement.UnitIndex>) {
const { elements } = unit
const { label_alt_id } = unit.model.atomicHierarchy.atoms
for (let i = 0, il = ringAtoms.length; i < il; ++i) {
const ei = elements[ringAtoms[i]]
const altId = label_alt_id.value(ei)
if (altId) return altId
} }
const index = indexC1 !== -1 ? indexC1 return ''
: indexC1X !== -1 ? indexC1X }
: indexC !== -1 ? indexC
: elements[indices[0]] function getAltId(unit: Unit.Atomic, index: StructureElement.UnitIndex) {
const { elements } = unit
const { label_alt_id } = unit.model.atomicHierarchy.atoms
return label_alt_id.value(elements[index])
}
function getDirection(direction: Vec3, unit: Unit.Atomic, index: ElementIndex, center: Vec3) {
const { position } = unit.conformation
Vec3.normalize(direction, Vec3.sub(direction, center, position(index, direction))) Vec3.normalize(direction, Vec3.sub(direction, center, position(index, direction)))
return direction return direction
} }
function getAtomId(unit: Unit.Atomic, index: number) { function getAtomId(unit: Unit.Atomic, index: StructureElement.UnitIndex) {
const { elements } = unit const { elements } = unit
const { label_atom_id } = unit.model.atomicHierarchy.atoms const { label_atom_id } = unit.model.atomicHierarchy.atoms
return label_atom_id.value(elements[index]) return label_atom_id.value(elements[index])
} }
function filterFusedRings(unitRings: UnitRings, rings: UnitRings.Index[] | undefined) {
if (!rings || !rings.length) return
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)) {
fusedRings.add(rings[rc[0]])
fusedRings.add(rings[rc[1]])
}
}
if (fusedRings.size) {
const filteredRings: UnitRings.Index[] = []
for (let i = 0, il = rings.length; i < il; ++i) {
if (!fusedRings.has(rings[i])) filteredRings.push(rings[i])
}
return filteredRings
} else {
return rings
}
}
export function computeCarbohydrates(structure: Structure): Carbohydrates { export function computeCarbohydrates(structure: Structure): Carbohydrates {
const links: CarbohydrateLink[] = [] const links: CarbohydrateLink[] = []
...@@ -62,8 +125,8 @@ export function computeCarbohydrates(structure: Structure): Carbohydrates { ...@@ -62,8 +125,8 @@ export function computeCarbohydrates(structure: Structure): Carbohydrates {
const elementsWithRingMap = new Map<string, number>() const elementsWithRingMap = new Map<string, number>()
function elementKey(residueIndex: number, unitId: number) { function elementKey(residueIndex: number, unitId: number, altId: string) {
return `${residueIndex}|${unitId}` return `${residueIndex}|${unitId}|${altId}`
} }
function fixLinkDirection(iA: number, iB: number) { function fixLinkDirection(iA: number, iB: number) {
...@@ -107,7 +170,7 @@ export function computeCarbohydrates(structure: Structure): Carbohydrates { ...@@ -107,7 +170,7 @@ export function computeCarbohydrates(structure: Structure): Carbohydrates {
sugarResidueMap = UnitRings.byFingerprintAndResidue(unit.rings, SugarRingFps); sugarResidueMap = UnitRings.byFingerprintAndResidue(unit.rings, SugarRingFps);
} }
const sugarRings = sugarResidueMap.get(residueIndex); const sugarRings = filterFusedRings(unit.rings, sugarResidueMap.get(residueIndex));
if (!sugarRings || !sugarRings.length) { if (!sugarRings || !sugarRings.length) {
elements.push({ elements.push({
...@@ -122,16 +185,18 @@ export function computeCarbohydrates(structure: Structure): Carbohydrates { ...@@ -122,16 +185,18 @@ export function computeCarbohydrates(structure: Structure): Carbohydrates {
for (let j = 0, jl = sugarRings.length; j < jl; ++j) { for (let j = 0, jl = sugarRings.length; j < jl; ++j) {
const ringAtoms = rings.all[sugarRings[j]]; const ringAtoms = rings.all[sugarRings[j]];
const anomericCarbon = getAnomericCarbon(unit, ringAtoms)
const pa = new PrincipalAxes(getPositionMatrix(unit, ringAtoms)) const pa = new PrincipalAxes(getPositionMatrix(unit, ringAtoms))
const center = Vec3.copy(Vec3.zero(), pa.center) const center = Vec3.copy(Vec3.zero(), pa.center)
const normal = Vec3.copy(Vec3.zero(), pa.normVecC) const normal = Vec3.copy(Vec3.zero(), pa.normVecC)
const direction = getDirection(Vec3.zero(), unit, ringAtoms, center) const direction = getDirection(Vec3.zero(), unit, anomericCarbon, center)
Vec3.orthogonalize(direction, normal, direction) Vec3.orthogonalize(direction, normal, direction)
const altId = getRingAltId(unit, ringAtoms)
const elementIndex = elements.length const elementIndex = elements.length
ringElements.push(elementIndex) ringElements.push(elementIndex)
elementsWithRingMap.set(elementKey(residueIndex, unit.id), elementIndex) elementsWithRingMap.set(elementKey(residueIndex, unit.id, altId), elementIndex)
elements.push({ elements.push({
geometry: { center, normal, direction }, geometry: { center, normal, direction },
hasRing: true, hasRing: true,
...@@ -144,8 +209,10 @@ export function computeCarbohydrates(structure: Structure): Carbohydrates { ...@@ -144,8 +209,10 @@ export function computeCarbohydrates(structure: Structure): Carbohydrates {
for (let j = 0, jl = ringCombinations.length; j < jl; ++j) { for (let j = 0, jl = ringCombinations.length; j < jl; ++j) {
const rc = ringCombinations[j]; const rc = ringCombinations[j];
const r0 = rings.all[sugarRings[rc[0]]], r1 = rings.all[sugarRings[rc[1]]]; const r0 = rings.all[sugarRings[rc[0]]], r1 = rings.all[sugarRings[rc[1]]];
if (IntAdjacencyGraph.areVertexSetsConnected(unit.links, r0, r1, 2)) { // 1,6 glycosidic links are distance 3 and 1,4 glycosidic links are distance 2
// fix both directions as it is unclear where the C1 atom is if (IntAdjacencyGraph.areVertexSetsConnected(unit.links, r0, r1, 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(ringElements[rc[0]], ringElements[rc[1]]) fixLinkDirection(ringElements[rc[0]], ringElements[rc[1]])
fixLinkDirection(ringElements[rc[1]], ringElements[rc[0]]) fixLinkDirection(ringElements[rc[1]], ringElements[rc[0]])
links.push({ links.push({
...@@ -162,6 +229,10 @@ export function computeCarbohydrates(structure: Structure): Carbohydrates { ...@@ -162,6 +229,10 @@ export function computeCarbohydrates(structure: Structure): Carbohydrates {
} }
} }
function getElementIndex(unit: Unit.Atomic, index: StructureElement.UnitIndex) {
return elementsWithRingMap.get(elementKey(unit.getResidueIndex(index), unit.id, getAltId(unit, index)))
}
// get carbohydrate links induced by inter-unit bonds // get carbohydrate links induced by inter-unit bonds
for (let i = 0, il = structure.units.length; i < il; ++i) { for (let i = 0, il = structure.units.length; i < il; ++i) {
const unit = structure.units[i] const unit = structure.units[i]
...@@ -172,8 +243,8 @@ export function computeCarbohydrates(structure: Structure): Carbohydrates { ...@@ -172,8 +243,8 @@ export function computeCarbohydrates(structure: Structure): Carbohydrates {
pairBonds.getBonds(indexA).forEach(bondInfo => { pairBonds.getBonds(indexA).forEach(bondInfo => {
const { unitA, unitB } = pairBonds const { unitA, unitB } = pairBonds
const indexB = bondInfo.indexB const indexB = bondInfo.indexB
const elementIndexA = elementsWithRingMap.get(elementKey(unitA.getResidueIndex(indexA), unitA.id)) const elementIndexA = getElementIndex(unitA, indexA)
const elementIndexB = elementsWithRingMap.get(elementKey(unitB.getResidueIndex(indexB), unitB.id)) const elementIndexB = getElementIndex(unitB, indexB)
if (elementIndexA !== undefined && elementIndexB !== undefined) { if (elementIndexA !== undefined && elementIndexB !== undefined) {
const atomIdA = getAtomId(unitA, indexA) const atomIdA = getAtomId(unitA, indexA)
......
...@@ -106,6 +106,7 @@ export class Stage { ...@@ -106,6 +106,7 @@ export class Stage {
// this.loadPdbid('4zs9') // contains raffinose // this.loadPdbid('4zs9') // contains raffinose
// this.loadPdbid('2yft') // contains kestose // this.loadPdbid('2yft') // contains kestose
this.loadPdbid('2b5t') // contains large carbohydrate polymer this.loadPdbid('2b5t') // contains large carbohydrate polymer
// this.loadPdbid('1b5f') // contains carbohydrate with alternate locations
// this.loadMmcifUrl(`../../examples/1cbs_full.bcif`) // this.loadMmcifUrl(`../../examples/1cbs_full.bcif`)
// this.loadMmcifUrl(`../../examples/1cbs_updated.cif`) // this.loadMmcifUrl(`../../examples/1cbs_updated.cif`)
// this.loadMmcifUrl(`../../examples/1crn.cif`) // this.loadMmcifUrl(`../../examples/1crn.cif`)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment