From b863aaedb364b53ecbdce671719fa142706f7ae4 Mon Sep 17 00:00:00 2001 From: Alexander Rose <alex.rose@rcsb.org> Date: Fri, 10 Jan 2020 15:33:06 -0800 Subject: [PATCH] general support for aromatic rings - containing annotated aromatic bonds - being flat with certain elements --- .../computed/interactions/charged.ts | 38 ++--------- .../structure/query/queries/generators.ts | 13 +++- .../structure/structure/unit/rings.ts | 64 ++++++++++++++++++- src/mol-plugin-ui/structure/selection.tsx | 3 +- .../util/structure-selection-helper.ts | 5 ++ .../language/symbol-table/structure-query.ts | 5 +- src/mol-script/runtime/query/table.ts | 2 +- 7 files changed, 89 insertions(+), 41 deletions(-) diff --git a/src/mol-model-props/computed/interactions/charged.ts b/src/mol-model-props/computed/interactions/charged.ts index 8db9c4805..b12631f00 100644 --- a/src/mol-model-props/computed/interactions/charged.ts +++ b/src/mol-model-props/computed/interactions/charged.ts @@ -10,17 +10,15 @@ import { ParamDefinition as PD } from '../../../mol-util/param-definition'; import { Structure, Unit, StructureElement } from '../../../mol-model/structure'; import { FeaturesBuilder, Features } from './features'; -import { ProteinBackboneAtoms, PolymerNames, BaseNames, ElementSymbol } from '../../../mol-model/structure/model/types'; -import { typeSymbol, atomId, eachBondedAtom, compId } from '../chemistry/util'; +import { ProteinBackboneAtoms, PolymerNames, BaseNames } from '../../../mol-model/structure/model/types'; +import { typeSymbol, atomId, eachBondedAtom } from '../chemistry/util'; import { Elements } from '../../../mol-model/structure/model/properties/atomic/types'; import { ValenceModelProvider } from '../valence-model'; import { degToRad } from '../../../mol-math/misc'; import { FeatureType, FeatureGroup, InteractionType } from './common'; import { ContactProvider } from './contacts'; -import { Segmentation, SortedArray } from '../../../mol-data/int'; +import { Segmentation } from '../../../mol-data/int'; import { isGuanidine, isAcetamidine, isPhosphate, isSulfonicAcid, isSulfate, isCarboxylate } from '../chemistry/functional-group'; -import { PrincipalAxes } from '../../../mol-math/linear-algebra/matrix/principal-axes'; -import { getPositions } from '../../../mol-model/structure/util'; import { Vec3 } from '../../../mol-math/linear-algebra'; const IonicParams = { @@ -189,38 +187,12 @@ function addUnitNegativeCharges(structure: Structure, unit: Unit.Atomic, builder } } -const AromaticRingElements = [ - Elements.B, Elements.C, Elements.N, Elements.O, - Elements.SI, Elements.P, Elements.S, - Elements.GE, Elements.AS, - Elements.SN, Elements.SB, - Elements.BI -] as ElementSymbol[] -const AromaticRingPlanarityThreshold = 0.05 - -function isRingAromatic(unit: Unit.Atomic, ring: SortedArray<StructureElement.UnitIndex>) { - // ignore Proline (can be flat because of bad geometry) - if (compId(unit, ring[0]) === 'PRO') return - // TODO also check `chem_comp_bond.pdbx_aromatic_flag` - let hasAromaticRingElement = false - for (let i = 0, il = ring.length; i < il; ++i) { - if (AromaticRingElements.includes(typeSymbol(unit, ring[i]))) { - hasAromaticRingElement = true - break - } - } - if (!hasAromaticRingElement) return - - const ma = PrincipalAxes.calculateMomentsAxes(getPositions(unit, ring)) - return Vec3.magnitude(ma.dirC) < AromaticRingPlanarityThreshold -} - function addUnitAromaticRings(structure: Structure, unit: Unit.Atomic, builder: FeaturesBuilder) { const { elements } = unit const { x, y, z } = unit.model.atomicConformation - for (const ring of unit.rings.all) { - if (!isRingAromatic(unit, ring)) continue + for (const ringIndex of unit.rings.aromaticRings) { + const ring = unit.rings.all[ringIndex] builder.startState() for (let i = 0, il = ring.length; i < il; ++i) { const j = ring[i] diff --git a/src/mol-model/structure/query/queries/generators.ts b/src/mol-model/structure/query/queries/generators.ts index 906d70c6b..646f67dfc 100644 --- a/src/mol-model/structure/query/queries/generators.ts +++ b/src/mol-model/structure/query/queries/generators.ts @@ -223,7 +223,7 @@ function getRingStructure(unit: Unit.Atomic, ring: UnitRing, inputStructure: Str return Structure.create([unit.getChild(SortedArray.ofSortedArray(elements))], { parent: inputStructure }); } -export function rings(fingerprints?: ArrayLike<UnitRing.Fingerprint>): StructureQuery { +export function rings(fingerprints?: ArrayLike<UnitRing.Fingerprint>, onlyAromatic?: boolean): StructureQuery { return function query_rings(ctx) { const { units } = ctx.inputStructure; const ret = StructureSelection.LinearBuilder(ctx.inputStructure); @@ -232,8 +232,14 @@ export function rings(fingerprints?: ArrayLike<UnitRing.Fingerprint>): Structure for (const u of units) { if (!Unit.isAtomic(u)) continue; - for (const r of u.rings.all) { - ret.add(getRingStructure(u, r, ctx.inputStructure)); + if (onlyAromatic) { + for (const r of u.rings.aromaticRings) { + ret.add(getRingStructure(u, u.rings.all[r], ctx.inputStructure)); + } + } else { + for (const r of u.rings.all) { + ret.add(getRingStructure(u, r, ctx.inputStructure)); + } } } } else { @@ -247,6 +253,7 @@ export function rings(fingerprints?: ArrayLike<UnitRing.Fingerprint>): Structure for (const fp of uniqueFps.array) { if (!rings.byFingerprint.has(fp)) continue; for (const r of rings.byFingerprint.get(fp)!) { + if (onlyAromatic && !rings.aromaticRings.includes(r)) continue; ret.add(getRingStructure(u, rings.all[r], ctx.inputStructure)); } } diff --git a/src/mol-model/structure/structure/unit/rings.ts b/src/mol-model/structure/structure/unit/rings.ts index bb3f50d3c..d9f1024ef 100644 --- a/src/mol-model/structure/structure/unit/rings.ts +++ b/src/mol-model/structure/structure/unit/rings.ts @@ -1,7 +1,8 @@ /** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal <david.sehnal@gmail.com> + * @author Alexander Rose <alexander.rose@weirdbyte.de> */ import { computeRings, getFingerprint, createIndex } from './rings/compute' @@ -9,7 +10,11 @@ import Unit from '../unit'; import StructureElement from '../element'; import { SortedArray } from '../../../../mol-data/int'; import { ResidueIndex } from '../../model'; -import { ElementSymbol } from '../../model/types'; +import { ElementSymbol, BondType } from '../../model/types'; +import { Elements } from '../../model/properties/atomic/types'; +import { getPositions } from '../../util'; +import { PrincipalAxes } from '../../../../mol-math/linear-algebra/matrix/principal-axes'; +import { Vec3 } from '../../../../mol-math/linear-algebra'; type UnitRing = SortedArray<StructureElement.UnitIndex> @@ -23,6 +28,7 @@ class UnitRings { readonly ringComponentIndex: ReadonlyArray<UnitRings.ComponentIndex>, readonly ringComponents: ReadonlyArray<ReadonlyArray<UnitRings.Index>> }; + private _aromaticRings?: ReadonlyArray<UnitRings.Index> private get index() { if (this._index) return this._index; @@ -50,6 +56,12 @@ class UnitRings { return this.index.ringComponents; } + get aromaticRings() { + if (this._aromaticRings) return this._aromaticRings; + this._aromaticRings = getAromaticRings(this.unit, this.all); + return this._aromaticRings; + } + constructor(all: ReadonlyArray<UnitRing>, public unit: Unit.Atomic) { this.all = all; } @@ -70,6 +82,47 @@ namespace UnitRing { export function elementFingerprint(elements: ArrayLike<ElementSymbol>) { return getFingerprint(elements as ArrayLike<string> as string[]) as Fingerprint; } + + const AromaticRingElements = new Set([ + Elements.B, Elements.C, Elements.N, Elements.O, + Elements.SI, Elements.P, Elements.S, + Elements.GE, Elements.AS, + Elements.SN, Elements.SB, + Elements.BI + ] as ElementSymbol[]) + const AromaticRingPlanarityThreshold = 0.05 + + export function isAromatic(unit: Unit.Atomic, ring: SortedArray<StructureElement.UnitIndex>): boolean { + const { elements, bonds: { b, offset, edgeProps: { flags } } } = unit; + const { type_symbol } = unit.model.atomicHierarchy.atoms; + const { label_comp_id } = unit.model.atomicHierarchy.residues; + + // ignore Proline (can be flat because of bad geometry) + if (label_comp_id.value(unit.getResidueIndex(ring[0])) === 'PRO') return false + + let aromaticBondCount = 0 + let hasAromaticRingElement = false + + for (let i = 0, il = ring.length; i < il; ++i) { + const aI = ring[i] + if (!hasAromaticRingElement && AromaticRingElements.has(type_symbol.value(elements[aI]))) { + hasAromaticRingElement = true + } + + for (let j = offset[aI], jl = offset[aI + 1]; j < jl; ++j) { + // comes e.g. from `chem_comp_bond.pdbx_aromatic_flag` + if (BondType.is(BondType.Flag.Aromatic, flags[j])) { + if (SortedArray.has(ring, b[j])) aromaticBondCount += 1 + + } + } + } + if (aromaticBondCount === 2 * ring.length) return true + if (!hasAromaticRingElement) return false + + const ma = PrincipalAxes.calculateMomentsAxes(getPositions(unit, ring)) + return Vec3.magnitude(ma.dirC) < AromaticRingPlanarityThreshold + } } namespace UnitRings { @@ -126,5 +179,12 @@ function addSingleResidueRings(rings: UnitRings, fp: UnitRing.Fingerprint, map: } } +function getAromaticRings(unit: Unit.Atomic, rings: ReadonlyArray<UnitRing>): ReadonlyArray<UnitRings.Index> { + const aromaticRings: UnitRings.Index[] = [] + for (let i = 0 as UnitRings.Index, il = rings.length; i < il; ++i) { + if (UnitRing.isAromatic(unit, rings[i])) aromaticRings.push(i) + } + return aromaticRings +} export { UnitRing, UnitRings } \ No newline at end of file diff --git a/src/mol-plugin-ui/structure/selection.tsx b/src/mol-plugin-ui/structure/selection.tsx index ea6d6b1a3..aa1000edb 100644 --- a/src/mol-plugin-ui/structure/selection.tsx +++ b/src/mol-plugin-ui/structure/selection.tsx @@ -19,7 +19,8 @@ const SSQ = StructureSelectionQueries const DefaultQueries: (keyof typeof SSQ)[] = [ 'all', 'polymer', 'trace', 'backbone', 'protein', 'nucleic', 'helix', 'beta', - 'water', 'branched', 'ligand', 'nonStandardPolymer', 'ring', + 'water', 'branched', 'ligand', 'nonStandardPolymer', + 'ring', 'aromaticRing', 'surroundings', 'complement', 'bonded' ] diff --git a/src/mol-plugin/util/structure-selection-helper.ts b/src/mol-plugin/util/structure-selection-helper.ts index a24f03399..0c8f3916a 100644 --- a/src/mol-plugin/util/structure-selection-helper.ts +++ b/src/mol-plugin/util/structure-selection-helper.ts @@ -298,6 +298,10 @@ const ring = StructureSelectionQuery('Rings in Residues', MS.struct.modifier.uni MS.struct.generator.rings() ])) +const aromaticRing = StructureSelectionQuery('Aromatic Rings in Residues', MS.struct.modifier.union([ + MS.struct.generator.rings({ 'only-aromatic': true }) +])) + const surroundings = StructureSelectionQuery('Surrounding Residues (5 \u212B) of Selection', MS.struct.modifier.union([ MS.struct.modifier.exceptBy({ 0: MS.struct.modifier.includeSurroundings({ @@ -345,6 +349,7 @@ export const StructureSelectionQueries = { nonStandardPolymer, coarse, ring, + aromaticRing, surroundings, complement, bonded, diff --git a/src/mol-script/language/symbol-table/structure-query.ts b/src/mol-script/language/symbol-table/structure-query.ts index 2e1657038..d76329a1f 100644 --- a/src/mol-script/language/symbol-table/structure-query.ts +++ b/src/mol-script/language/symbol-table/structure-query.ts @@ -99,7 +99,10 @@ const generator = { // 'group-by': Argument(Type.Any, { isOptional: true, defaultValue: ``, description: 'Group the bonds using the privided value' }), }), Types.ElementSelectionQuery, 'Return all pairs of atoms for which the test is satisfied.'), - rings: symbol(Arguments.List(Types.RingFingerprint), Types.ElementSelectionQuery, 'Return rings with the specified fingerprint(s). If no fingerprints are given, return all rings.'), + rings: symbol(Arguments.Dictionary({ + 'fingerprint': Argument(Types.RingFingerprint, { isOptional: true }), + 'only-aromatic': Argument(Type.Bool, { isOptional: true, defaultValue: false }), + }), Types.ElementSelectionQuery, 'Return all rings or those with the specified fingerprint and/or only aromatic rings.'), queryInSelection: symbol(Arguments.Dictionary({ 0: Argument(Types.ElementSelectionQuery), diff --git a/src/mol-script/runtime/query/table.ts b/src/mol-script/runtime/query/table.ts index 4eaa59ebc..75a9104a8 100644 --- a/src/mol-script/runtime/query/table.ts +++ b/src/mol-script/runtime/query/table.ts @@ -239,7 +239,7 @@ const symbols = [ D(MolScript.structureQuery.generator.all, function structureQuery_generator_all(ctx) { return Queries.generators.all(ctx) }), D(MolScript.structureQuery.generator.empty, function structureQuery_generator_empty(ctx) { return Queries.generators.none(ctx) }), D(MolScript.structureQuery.generator.bondedAtomicPairs, function structureQuery_generator_bondedAtomicPairs(ctx, xs) { return Queries.generators.bondedAtomicPairs(xs && xs[0])(ctx) }), - D(MolScript.structureQuery.generator.rings, function structureQuery_generator_rings(ctx, xs) { return Queries.generators.rings(getArray(ctx, xs))(ctx) }), + D(MolScript.structureQuery.generator.rings, function structureQuery_generator_rings(ctx, xs) { return Queries.generators.rings(xs?.['fingerprint']?.(ctx) as any, xs?.['only-aromatic']?.(ctx))(ctx) }), // ============= MODIFIERS ================ -- GitLab