From 900c8d6e92f47cfe2dd4f834d01da83fd1b26c19 Mon Sep 17 00:00:00 2001 From: Alexander Rose <alex.rose@rcsb.org> Date: Tue, 26 Jun 2018 20:20:32 -0700 Subject: [PATCH] added cross-link distance restraints --- src/mol-app/ui/transform/ball-and-stick.tsx | 5 +- src/mol-app/ui/transform/spacefill.tsx | 5 +- .../structure/distance-restraint.ts | 55 ++++++ .../visual/cross-link-restraint-cylinder.ts | 175 ++++++++++++++++++ src/mol-geo/representation/util.ts | 6 +- src/mol-geo/theme/index.ts | 18 ++ .../model/formats/mmcif/pair-restraint.ts | 157 ++++++++++++++++ .../structure/structure/structure.ts | 9 + .../structure/unit/pair-restraints.ts | 9 + .../structure/unit/pair-restraints/data.ts | 60 ++++++ .../pair-restraints/extract-cross-links.ts | 105 +++++++++++ .../extract-predicted-contacts.ts | 7 + src/mol-view/stage.ts | 20 +- src/mol-view/state/entity.ts | 8 + src/mol-view/state/transform.ts | 71 ++++--- 15 files changed, 674 insertions(+), 36 deletions(-) create mode 100644 src/mol-geo/representation/structure/distance-restraint.ts create mode 100644 src/mol-geo/representation/structure/visual/cross-link-restraint-cylinder.ts create mode 100644 src/mol-model/structure/model/formats/mmcif/pair-restraint.ts create mode 100644 src/mol-model/structure/structure/unit/pair-restraints.ts create mode 100644 src/mol-model/structure/structure/unit/pair-restraints/data.ts create mode 100644 src/mol-model/structure/structure/unit/pair-restraints/extract-cross-links.ts create mode 100644 src/mol-model/structure/structure/unit/pair-restraints/extract-predicted-contacts.ts diff --git a/src/mol-app/ui/transform/ball-and-stick.tsx b/src/mol-app/ui/transform/ball-and-stick.tsx index 82cf45d7e..d597e51bb 100644 --- a/src/mol-app/ui/transform/ball-and-stick.tsx +++ b/src/mol-app/ui/transform/ball-and-stick.tsx @@ -19,6 +19,7 @@ import { ColorTheme, SizeTheme } from 'mol-geo/theme'; import { Color, ColorNames } from 'mol-util/color'; import { Slider } from '../controls/slider'; import { VisualQuality } from 'mol-geo/representation/util'; +import { Unit } from 'mol-model/structure'; export const ColorThemeInfo = { 'atom-index': {}, @@ -46,6 +47,7 @@ interface BallAndStickState { linkRadius: number radialSegments: number detail: number + unitKinds: Unit.Kind[] } export class BallAndStick extends View<Controller<any>, BallAndStickState, { transform: BallAndStickUpdate, entity: BallAndStickEntity, ctx: StateContext }> { @@ -65,7 +67,8 @@ export class BallAndStick extends View<Controller<any>, BallAndStickState, { tra linkSpacing: 1, linkRadius: 0.25, radialSegments: 16, - detail: 1 + detail: 1, + unitKinds: [] as Unit.Kind[] } componentWillMount() { diff --git a/src/mol-app/ui/transform/spacefill.tsx b/src/mol-app/ui/transform/spacefill.tsx index 2da65fe81..55377180a 100644 --- a/src/mol-app/ui/transform/spacefill.tsx +++ b/src/mol-app/ui/transform/spacefill.tsx @@ -19,6 +19,7 @@ import { ColorTheme, SizeTheme } from 'mol-geo/theme'; import { Color, ColorNames } from 'mol-util/color'; import { Slider } from '../controls/slider'; import { VisualQuality } from 'mol-geo/representation/util'; +import { Unit } from 'mol-model/structure'; export const ColorThemeInfo = { 'atom-index': {}, @@ -42,6 +43,7 @@ interface SpacefillState { depthMask: boolean useFog: boolean quality: VisualQuality + unitKinds: Unit.Kind[] } export class Spacefill extends View<Controller<any>, SpacefillState, { transform: SpacefillUpdate, entity: SpacefillEntity, ctx: StateContext }> { @@ -57,7 +59,8 @@ export class Spacefill extends View<Controller<any>, SpacefillState, { transform alpha: 1, depthMask: true, useFog: true, - quality: 'auto' as VisualQuality + quality: 'auto' as VisualQuality, + unitKinds: [] as Unit.Kind[] } componentWillMount() { diff --git a/src/mol-geo/representation/structure/distance-restraint.ts b/src/mol-geo/representation/structure/distance-restraint.ts new file mode 100644 index 000000000..9cb1dd01c --- /dev/null +++ b/src/mol-geo/representation/structure/distance-restraint.ts @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { StructureRepresentation } from '.'; +import { PickingId } from '../../util/picking'; +import { Structure } from 'mol-model/structure'; +import { Task } from 'mol-task'; +import { Loci } from 'mol-model/loci'; +import { MarkerAction } from '../../util/marker-data'; +import { SizeTheme } from '../../theme'; +import { CrossLinkRestraintVisual, DefaultCrossLinkRestraintProps } from './visual/cross-link-restraint-cylinder'; + +export const DefaultDistanceRestraintProps = { + ...DefaultCrossLinkRestraintProps, + + sizeTheme: { name: 'uniform', value: 0.25 } as SizeTheme, +} +export type DistanceRestraintProps = Partial<typeof DefaultDistanceRestraintProps> + +export function DistanceRestraintRepresentation(): StructureRepresentation<DistanceRestraintProps> { + const crossLinkRepr = StructureRepresentation(CrossLinkRestraintVisual) + + return { + get renderObjects() { + return [ ...crossLinkRepr.renderObjects ] + }, + get props() { + return { ...crossLinkRepr.props } + }, + create: (structure: Structure, props: DistanceRestraintProps = {} as DistanceRestraintProps) => { + const p = Object.assign({}, DefaultDistanceRestraintProps, props) + return Task.create('DistanceRestraintRepresentation', async ctx => { + await crossLinkRepr.create(structure, p).runInContext(ctx) + }) + }, + update: (props: DistanceRestraintProps) => { + const p = Object.assign({}, props) + return Task.create('Updating DistanceRestraintRepresentation', async ctx => { + await crossLinkRepr.update(p).runInContext(ctx) + }) + }, + getLoci: (pickingId: PickingId) => { + return crossLinkRepr.getLoci(pickingId) + }, + mark: (loci: Loci, action: MarkerAction) => { + crossLinkRepr.mark(loci, action) + }, + destroy() { + crossLinkRepr.destroy() + } + } +} \ No newline at end of file diff --git a/src/mol-geo/representation/structure/visual/cross-link-restraint-cylinder.ts b/src/mol-geo/representation/structure/visual/cross-link-restraint-cylinder.ts new file mode 100644 index 000000000..e03979728 --- /dev/null +++ b/src/mol-geo/representation/structure/visual/cross-link-restraint-cylinder.ts @@ -0,0 +1,175 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { ValueCell } from 'mol-util/value-cell' + +import { RenderObject, createMeshRenderObject, MeshRenderObject } from 'mol-gl/render-object' +import { Link, Structure } from 'mol-model/structure'; +import { DefaultStructureProps, StructureVisual } from '../index'; +import { RuntimeContext } from 'mol-task' +import { LinkCylinderProps, DefaultLinkCylinderProps, createLinkCylinderMesh } from './util/link'; +import { MeshValues } from 'mol-gl/renderable'; +import { getMeshData } from '../../../util/mesh-data'; +import { Mesh } from '../../../shape/mesh'; +import { PickingId } from '../../../util/picking'; +import { Vec3 } from 'mol-math/linear-algebra'; +import { createUniformColor } from '../../../util/color-data'; +import { Loci, isEveryLoci, EmptyLoci } from 'mol-model/loci'; +import { MarkerAction, applyMarkerAction, createMarkers, MarkerData } from '../../../util/marker-data'; +import { SizeTheme } from '../../../theme'; +import { createIdentityTransform } from './util/common'; +import { updateMeshValues, updateRenderableState, createMeshValues, createRenderableState } from '../../util'; +// import { chainIdLinkColorData } from '../../../theme/structure/color/chain-id'; + +async function createCrossLinkRestraintCylinderMesh(ctx: RuntimeContext, structure: Structure, props: LinkCylinderProps, mesh?: Mesh) { + + const crossLinks = structure.crossLinkRestraints + if (!crossLinks.count) return Mesh.createEmpty(mesh) + + const builderProps = { + linkCount: crossLinks.count, + referencePosition: (edgeIndex: number) => null, + position: (posA: Vec3, posB: Vec3, edgeIndex: number) => { + const b = crossLinks.pairs[edgeIndex] + // console.log(b) + const uA = b.unitA, uB = b.unitB + uA.conformation.position(uA.elements[b.indexA], posA) + uB.conformation.position(uB.elements[b.indexB], posB) + // console.log(posA, posB) + }, + order: (edgeIndex: number) => 1, + flags: (edgeIndex: number) => 0 + } + + return createLinkCylinderMesh(ctx, builderProps, props, mesh) +} + +export const DefaultCrossLinkRestraintProps = { + ...DefaultStructureProps, + ...DefaultLinkCylinderProps, + sizeTheme: { name: 'physical', factor: 0.3 } as SizeTheme, + flipSided: false, + flatShaded: false, +} +export type CrossLinkRestraintProps = Partial<typeof DefaultCrossLinkRestraintProps> + +export function CrossLinkRestraintVisual(): StructureVisual<CrossLinkRestraintProps> { + const renderObjects: RenderObject[] = [] + let cylinders: MeshRenderObject + let currentProps: typeof DefaultCrossLinkRestraintProps + let mesh: Mesh + let currentStructure: Structure + + return { + renderObjects, + async create(ctx: RuntimeContext, structure: Structure, props: CrossLinkRestraintProps = {}) { + currentProps = Object.assign({}, DefaultCrossLinkRestraintProps, props) + + renderObjects.length = 0 // clear + currentStructure = structure + + const elementCount = structure.crossLinkRestraints.count + const instanceCount = 1 + + mesh = await createCrossLinkRestraintCylinderMesh(ctx, structure, currentProps) + + if (ctx.shouldUpdate) await ctx.update('Computing link transforms'); + const transforms = createIdentityTransform() + + if (ctx.shouldUpdate) await ctx.update('Computing link colors'); + const color = createUniformColor({ value: 0x119911 }) // TODO + + if (ctx.shouldUpdate) await ctx.update('Computing link marks'); + const marker = createMarkers(instanceCount * elementCount) + + const counts = { drawCount: mesh.triangleCount * 3, elementCount, instanceCount } + + const values: MeshValues = { + ...getMeshData(mesh), + ...color, + ...marker, + aTransform: transforms, + elements: mesh.indexBuffer, + ...createMeshValues(currentProps, counts), + } + const state = createRenderableState(currentProps) + + cylinders = createMeshRenderObject(values, state) + console.log(values, instanceCount, elementCount) + renderObjects.push(cylinders) + }, + async update(ctx: RuntimeContext, props: CrossLinkRestraintProps) { + const newProps = Object.assign({}, currentProps, props) + + if (!cylinders) return false + + // TODO create in-place + if (currentProps.radialSegments !== newProps.radialSegments) return false + + updateMeshValues(cylinders.values, newProps) + updateRenderableState(cylinders.state, newProps) + + return false + }, + getLoci(pickingId: PickingId) { + return getLinkLoci(pickingId, currentStructure, cylinders.id) + }, + mark(loci: Loci, action: MarkerAction) { + markLink(loci, action, currentStructure, cylinders.values) + }, + destroy() { + // TODO + } + } +} + +function getLinkLoci(pickingId: PickingId, structure: Structure, id: number) { + const { objectId, elementId } = pickingId + if (id === objectId) { + const pair = structure.crossLinkRestraints.pairs[elementId] + if (pair) { + return Link.Loci([{ + aUnit: pair.unitA, + aIndex: pair.indexA, + bUnit: pair.unitB, + bIndex: pair.indexB + }]) + } + } + return EmptyLoci +} + +function markLink(loci: Loci, action: MarkerAction, structure: Structure, values: MarkerData) { + const tMarker = values.tMarker + + const crossLinks = structure.crossLinkRestraints + const elementCount = crossLinks.count + const instanceCount = 1 + + let changed = false + const array = tMarker.ref.value.array + if (isEveryLoci(loci)) { + applyMarkerAction(array, 0, elementCount * instanceCount, action) + changed = true + } else if (Link.isLoci(loci)) { + for (const b of loci.links) { + const indices = crossLinks.getPairIndices(b.aIndex, b.aUnit, b.bIndex, b.bUnit) + if (indices) { + for (let i = 0, il = indices.length; i < il; ++i) { + const idx = indices[i] + if (applyMarkerAction(array, idx, idx + 1, action) && !changed) { + changed = true + } + } + } + } + } else { + return + } + if (changed) { + ValueCell.update(tMarker, tMarker.ref.value) + } +} \ No newline at end of file diff --git a/src/mol-geo/representation/util.ts b/src/mol-geo/representation/util.ts index fdb8f988e..1d4fd4f02 100644 --- a/src/mol-geo/representation/util.ts +++ b/src/mol-geo/representation/util.ts @@ -103,15 +103,15 @@ export function getQualityProps(props: Partial<QualityProps>, structure: Structu switch (quality) { case 'highest': - detail = 3 + detail = 2 radialSegments = 36 break case 'high': - detail = 2 + detail = 1 radialSegments = 24 break case 'medium': - detail = 1 + detail = 0 radialSegments = 12 break case 'low': diff --git a/src/mol-geo/theme/index.ts b/src/mol-geo/theme/index.ts index 92b1bd849..a306838fc 100644 --- a/src/mol-geo/theme/index.ts +++ b/src/mol-geo/theme/index.ts @@ -5,6 +5,8 @@ */ import { Color } from 'mol-util/color'; +// import { Loci } from 'mol-model/loci'; +// import { Structure } from 'mol-model/structure'; export interface UniformColorTheme { name: 'uniform' @@ -16,6 +18,22 @@ export interface ScaleColorTheme { domain?: [number, number] } +// interface StructureColorProvider { +// uniform(): Color +// instance(instanceIdx: number): Color +// element(elementIdx: number): Color +// elementInstance(elementIdx: number, instanceIdx: number): Color + +// lociColor(loci: Loci): Color +// } + +// export namespace ColorProvider { +// export function fromLociColor(lociColor: (loci: Loci) => Color) { + +// return +// } +// } + export type ColorTheme = UniformColorTheme | ScaleColorTheme export interface UniformSizeTheme { diff --git a/src/mol-model/structure/model/formats/mmcif/pair-restraint.ts b/src/mol-model/structure/model/formats/mmcif/pair-restraint.ts new file mode 100644 index 000000000..5cfe9f4e3 --- /dev/null +++ b/src/mol-model/structure/model/formats/mmcif/pair-restraint.ts @@ -0,0 +1,157 @@ +/** + * Copyright (c) 2018 Mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +// ihm_predicted_contact_restraint: { +// id: int, +// entity_id_1: str, +// entity_id_2: str, +// asym_id_1: str, +// asym_id_2: str, +// comp_id_1: str, +// comp_id_2: str, +// seq_id_1: int, +// seq_id_2: int, +// atom_id_1: str, +// atom_id_2: str, +// distance_upper_limit: float, +// probability: float, +// restraint_type: Aliased<'lower bound' | 'upper bound' | 'lower and upper bound'>(str), +// model_granularity: Aliased<'by-residue' | 'by-feature' | 'by-atom'>(str), +// dataset_list_id: int, +// software_id: int, +// }, +// ihm_cross_link_restraint: { +// id: int, +// group_id: int, +// entity_id_1: str, +// entity_id_2: str, +// asym_id_1: str, +// asym_id_2: str, +// comp_id_1: str, +// comp_id_2: str, +// seq_id_1: int, +// seq_id_2: int, +// atom_id_1: str, +// atom_id_2: str, +// restraint_type: Aliased<'harmonic' | 'upper bound' | 'lower bound'>(str), +// conditional_crosslink_flag: Aliased<'ALL' | 'ANY'>(str), +// model_granularity: Aliased<'by-residue' | 'by-feature' | 'by-atom'>(str), +// distance_threshold: float, +// psi: float, +// sigma_1: float, +// sigma_2: float, +// }, + +import Model from '../../model' +import { Table } from 'mol-data/db' +import { mmCIF_Schema } from 'mol-io/reader/cif/schema/mmcif'; +import { findAtomIndexByLabelName } from './util'; +import { Element, Unit } from '../../../structure'; + +function findAtomIndex(model: Model, entityId: string, asymId: string, compId: string, seqId: number, atomId: string) { + if (!model.atomicHierarchy.atoms.auth_atom_id.isDefined) return -1 + const residueIndex = model.atomicHierarchy.findResidueKey(entityId, compId, asymId, seqId, '') + if (residueIndex < 0) return -1 + return findAtomIndexByLabelName(model, residueIndex, atomId, '') as Element +} + +// function findElementIndex(model: Model, entityId: string, asymId: string, compId: string, seqId: number, atomId: string) { + +// } + +// function key(entityId: string, asymId: string, compId: string, seqId: number, atomId: string) { +// return `${entityId}|${asymId}|${compId}|${seqId}|${atomId}` +// } + +export interface IHMCrossLinkRestraint { + getIndicesByElement: (element: Element, kind: Unit.Kind) => number[] + data: Table<mmCIF_Schema['ihm_cross_link_restraint']> +} + +export namespace IHMCrossLinkRestraint { + export const PropName = '__CrossLinkRestraint__'; + export function fromModel(model: Model): IHMCrossLinkRestraint | undefined { + if (model.properties[PropName]) return model.properties[PropName] + + if (model.sourceData.kind !== 'mmCIF') return + const { ihm_cross_link_restraint } = model.sourceData.data; + if (!ihm_cross_link_restraint._rowCount) return + + const p1 = { + entity_id: ihm_cross_link_restraint.entity_id_1, + asym_id: ihm_cross_link_restraint.asym_id_1, + comp_id: ihm_cross_link_restraint.comp_id_1, + seq_id: ihm_cross_link_restraint.seq_id_1, + atom_id: ihm_cross_link_restraint.atom_id_1, + } + + const p2: typeof p1 = { + entity_id: ihm_cross_link_restraint.entity_id_2, + asym_id: ihm_cross_link_restraint.asym_id_2, + comp_id: ihm_cross_link_restraint.comp_id_2, + seq_id: ihm_cross_link_restraint.seq_id_2, + atom_id: ihm_cross_link_restraint.atom_id_2, + } + + function _add(map: Map<Element, number[]>, element: Element, row: number) { + const indices = map.get(element) + if (indices) indices.push(row) + else map.set(element, [ row ]) + } + + function add(row: number, ps: typeof p1) { + const entityId = ps.entity_id.value(row) + const asymId = ps.asym_id.value(row) + const seqId = ps.seq_id.value(row) + + if (ihm_cross_link_restraint.model_granularity.value(row) === 'by-atom') { + const atomicElement = findAtomIndex(model, entityId, asymId, ps.comp_id.value(row), seqId, ps.atom_id.value(row)) + if (atomicElement >= 0) _add(atomicElementMap, atomicElement as Element, row) + } else if (model.coarseHierarchy.isDefined) { + const sphereElement = model.coarseHierarchy.spheres.findSequenceKey(entityId, asymId, seqId) + if (sphereElement >= 0) { + _add(sphereElementMap, sphereElement as Element, row) + } else { + const gaussianElement = model.coarseHierarchy.gaussians.findSequenceKey(entityId, asymId, seqId) + if (gaussianElement >= 0) _add(gaussianElementMap, gaussianElement as Element, row) + } + } + } + + function getMapByKind(kind: Unit.Kind) { + switch (kind) { + case Unit.Kind.Atomic: return atomicElementMap; + case Unit.Kind.Spheres: return sphereElementMap; + case Unit.Kind.Gaussians: return gaussianElementMap; + } + } + + /** map from atomic element to cross link indices */ + const atomicElementMap: Map<Element, number[]> = new Map() + /** map from sphere element to cross link indices */ + const sphereElementMap: Map<Element, number[]> = new Map() + /** map from gaussian element to cross link indices */ + const gaussianElementMap: Map<Element, number[]> = new Map() + + const emptyIndexArray: number[] = []; + + for (let i = 0; i < ihm_cross_link_restraint._rowCount; ++i) { + add(i, p1) + add(i, p2) + } + + const crossLinkRestraint = { + getIndicesByElement: (element: Element, kind: Unit.Kind) => { + const map = getMapByKind(kind) + const idx = map.get(element) + return idx !== undefined ? idx : emptyIndexArray + }, + data: ihm_cross_link_restraint + } + model.properties[PropName] = crossLinkRestraint + return crossLinkRestraint + } +} \ No newline at end of file diff --git a/src/mol-model/structure/structure/structure.ts b/src/mol-model/structure/structure/structure.ts index 09a9db336..90b1d3a5c 100644 --- a/src/mol-model/structure/structure/structure.ts +++ b/src/mol-model/structure/structure/structure.ts @@ -16,10 +16,12 @@ import { CoarseElements } from '../model/properties/coarse'; import { StructureSubsetBuilder } from './util/subset-builder'; import { Queries } from '../query'; import { InterUnitBonds, computeInterUnitBonds } from './unit/links'; +import { CrossLinkRestraints, extractCrossLinkRestraints } from './unit/pair-restraints'; class Structure { readonly unitMap: IntMap<Unit>; readonly units: ReadonlyArray<Unit>; + /** Count of all elements in the structure, i.e. the sum of the elements in the units */ readonly elementCount: number; private _hashCode = 0; @@ -68,6 +70,13 @@ class Structure { return this._links; } + private _crossLinkRestraints?: CrossLinkRestraints = void 0; + get crossLinkRestraints() { + if (this._crossLinkRestraints) return this._crossLinkRestraints; + this._crossLinkRestraints = extractCrossLinkRestraints(this); + return this._crossLinkRestraints; + } + constructor(units: ArrayLike<Unit>) { const map = IntMap.Mutable<Unit>(); let elementCount = 0; diff --git a/src/mol-model/structure/structure/unit/pair-restraints.ts b/src/mol-model/structure/structure/unit/pair-restraints.ts new file mode 100644 index 000000000..6d0d0bd78 --- /dev/null +++ b/src/mol-model/structure/structure/unit/pair-restraints.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +export * from './pair-restraints/data' +export * from './pair-restraints/extract-cross-links' +// export * from './pair-restraints/extract-predicted_contacts' diff --git a/src/mol-model/structure/structure/unit/pair-restraints/data.ts b/src/mol-model/structure/structure/unit/pair-restraints/data.ts new file mode 100644 index 000000000..0fdd3ce75 --- /dev/null +++ b/src/mol-model/structure/structure/unit/pair-restraints/data.ts @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import Unit from '../../unit'; + +const emptyArray: number[] = [] + +class CrossLinkRestraints { + readonly count: number + private readonly pairKeyIndices: Map<string, number[]> + + /** Indices into this.pairs */ + getPairIndices(indexA: number, unitA: Unit, indexB: number, unitB: Unit): ReadonlyArray<number> { + const key = CrossLinkRestraints.getPairKey(indexA, unitA, indexB, unitB) + const indices = this.pairKeyIndices.get(key) + return indices !== undefined ? indices : emptyArray + } + + getPairs(indexA: number, unitA: Unit, indexB: number, unitB: Unit): CrossLinkRestraints.Pair[] | undefined { + const indices = this.getPairIndices(indexA, unitA, indexB, unitB) + return indices.length ? indices.map(idx => this.pairs[idx]) : undefined + } + + constructor(public pairs: ReadonlyArray<CrossLinkRestraints.Pair>) { + const pairKeyIndices = new Map<string, number[]>() + this.pairs.forEach((p, i) => { + const key = CrossLinkRestraints.getPairKey(p.indexA, p.unitA, p.indexB, p.unitB) + const indices = pairKeyIndices.get(key) + if (indices) indices.push(i) + else pairKeyIndices.set(key, [i]) + }) + + this.count = pairs.length + this.pairKeyIndices = pairKeyIndices + } +} + +namespace CrossLinkRestraints { + export interface Pair { + readonly unitA: Unit, + readonly unitB: Unit, + readonly indexA: number, + readonly indexB: number, + + readonly restraintType: 'harmonic' | 'upper bound' | 'lower bound', + readonly distanceThreshold: number, + readonly psi: number, + readonly sigma1: number, + readonly sigma2: number, + } + + export function getPairKey(indexA: number, unitA: Unit, indexB: number, unitB: Unit) { + return `${indexA}|${unitA.id}|${indexB}|${unitB.id}` + } +} + +export { CrossLinkRestraints } \ No newline at end of file diff --git a/src/mol-model/structure/structure/unit/pair-restraints/extract-cross-links.ts b/src/mol-model/structure/structure/unit/pair-restraints/extract-cross-links.ts new file mode 100644 index 000000000..a69307757 --- /dev/null +++ b/src/mol-model/structure/structure/unit/pair-restraints/extract-cross-links.ts @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import Unit from '../../unit'; +import Structure from '../../structure'; +import { IHMCrossLinkRestraint } from '../../../model/formats/mmcif/pair-restraint'; +import { CrossLinkRestraints } from './data'; + +function _addRestraints(map: Map<number, number>, unit: Unit, restraints: IHMCrossLinkRestraint) { + const { elements } = unit; + const elementCount = elements.length; + const kind = unit.kind + + for (let i = 0; i < elementCount; i++) { + const e = elements[i]; + restraints.getIndicesByElement(e, kind).forEach(ri => map.set(ri, i)) + } +} + +function extractInter(pairs: CrossLinkRestraints.Pair[], unitA: Unit, unitB: Unit) { + if (unitA.model !== unitB.model) return + if (unitA.model.sourceData.kind !== 'mmCIF') return + + const restraints = IHMCrossLinkRestraint.fromModel(unitA.model) + if (!restraints) return + + const rA = new Map<number, number>(); + const rB = new Map<number, number>(); + _addRestraints(rA, unitA, restraints) + _addRestraints(rB, unitB, restraints) + + rA.forEach((indexA, ri) => { + const indexB = rB.get(ri) + if (indexB !== undefined) { + pairs.push( + createCrossLinkRestraint(unitA, indexA, unitB, indexB, restraints, ri), + createCrossLinkRestraint(unitB, indexB, unitA, indexA, restraints, ri) + ) + } + }) +} + +function extractIntra(pairs: CrossLinkRestraints.Pair[], unit: Unit) { + if (unit.model.sourceData.kind !== 'mmCIF') return + + const restraints = IHMCrossLinkRestraint.fromModel(unit.model) + if (!restraints) return + + const { elements } = unit; + const elementCount = elements.length; + const kind = unit.kind + + const r = new Map<number, number[]>(); + + for (let i = 0; i < elementCount; i++) { + const e = elements[i]; + restraints.getIndicesByElement(e, kind).forEach(ri => { + const il = r.get(ri) + if (il) il.push(i) + else r.set(ri, [i]) + }) + } + + r.forEach((il, ri) => { + if (il.length < 2) return + const [ indexA, indexB ] = il + pairs.push( + createCrossLinkRestraint(unit, indexA, unit, indexB, restraints, ri), + createCrossLinkRestraint(unit, indexB, unit, indexA, restraints, ri) + ) + }) +} + +function createCrossLinkRestraint(unitA: Unit, indexA: number, unitB: Unit, indexB: number, restraints: IHMCrossLinkRestraint, row: number): CrossLinkRestraints.Pair { + return { + unitA, indexA, unitB, indexB, + + restraintType: restraints.data.restraint_type.value(row), + distanceThreshold: restraints.data.distance_threshold.value(row), + psi: restraints.data.psi.value(row), + sigma1: restraints.data.sigma_1.value(row), + sigma2: restraints.data.sigma_2.value(row), + } +} + +function extractCrossLinkRestraints(structure: Structure): CrossLinkRestraints { + const pairs: CrossLinkRestraints.Pair[] = [] + + const n = structure.units.length + for (let i = 0; i < n; ++i) { + const unitA = structure.units[i] + extractIntra(pairs, unitA) + for (let j = i + 1; j < n; ++j) { + const unitB = structure.units[j] + extractInter(pairs, unitA, unitB) + } + } + + return new CrossLinkRestraints(pairs) +} + +export { extractCrossLinkRestraints }; diff --git a/src/mol-model/structure/structure/unit/pair-restraints/extract-predicted-contacts.ts b/src/mol-model/structure/structure/unit/pair-restraints/extract-predicted-contacts.ts new file mode 100644 index 000000000..ad6d43f51 --- /dev/null +++ b/src/mol-model/structure/structure/unit/pair-restraints/extract-predicted-contacts.ts @@ -0,0 +1,7 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +// TODO extract from `ihm_predicted_contact_restraint` \ No newline at end of file diff --git a/src/mol-view/stage.ts b/src/mol-view/stage.ts index 097f79f2c..337cfba32 100644 --- a/src/mol-view/stage.ts +++ b/src/mol-view/stage.ts @@ -7,7 +7,7 @@ import Viewer from 'mol-view/viewer' import { StateContext } from './state/context'; import { Progress } from 'mol-task'; -import { MmcifUrlToModel, ModelToStructure, StructureToSpacefill, StructureToBallAndStick } from './state/transform'; +import { MmcifUrlToModel, ModelToStructure, StructureToSpacefill, StructureToBallAndStick, StructureToDistanceRestraint } from './state/transform'; import { UrlEntity } from './state/entity'; import { SpacefillProps } from 'mol-geo/representation/structure/spacefill'; import { Context } from 'mol-app/context/context'; @@ -15,15 +15,17 @@ import { BallAndStickProps } from 'mol-geo/representation/structure/ball-and-sti const spacefillProps: SpacefillProps = { doubleSided: true, - colorTheme: { name: 'atom-index' }, - quality: 'medium' + colorTheme: { name: 'chain-id' }, + quality: 'auto', + useFog: false } const ballAndStickProps: BallAndStickProps = { doubleSided: true, colorTheme: { name: 'chain-id' }, sizeTheme: { name: 'uniform', value: 0.25 }, - quality: 'medium' + quality: 'auto', + useFog: false } export class Stage { @@ -42,10 +44,13 @@ export class Stage { // this.loadPdbid('1jj2') // this.loadPdbid('4umt') // ligand has bond with order 3 // this.loadPdbid('1crn') // small + // this.loadPdbid('1rb8') // virus // this.loadPdbid('1blu') // metal coordination - this.loadPdbid('3pqr') // inter unit bonds + // this.loadPdbid('3pqr') // inter unit bonds // this.loadPdbid('4v5a') // ribosome // this.loadMmcifUrl(`../../examples/1cbs_full.bcif`) + + this.loadMmcifUrl(`../../../test/pdb-dev/PDBDEV_00000001.cif`) } async loadMmcifUrl (url: string) { @@ -53,8 +58,9 @@ export class Stage { const modelEntity = await MmcifUrlToModel.apply(this.ctx, urlEntity) const structureEntity = await ModelToStructure.apply(this.ctx, modelEntity) - StructureToSpacefill.apply(this.ctx, structureEntity, { ...spacefillProps, visible: false }) - StructureToBallAndStick.apply(this.ctx, structureEntity, ballAndStickProps) + StructureToSpacefill.apply(this.ctx, structureEntity, { ...spacefillProps, visible: true }) + // StructureToBallAndStick.apply(this.ctx, structureEntity, ballAndStickProps) + StructureToDistanceRestraint.apply(this.ctx, structureEntity, ballAndStickProps) this.globalContext.components.sequenceView.setState({ structure: structureEntity.value }); } diff --git a/src/mol-view/state/entity.ts b/src/mol-view/state/entity.ts index 2b089ed13..d106b4f76 100644 --- a/src/mol-view/state/entity.ts +++ b/src/mol-view/state/entity.ts @@ -14,6 +14,7 @@ import { Model, Structure } from 'mol-model/structure'; import { StructureRepresentation } from 'mol-geo/representation/structure'; import { SpacefillProps } from 'mol-geo/representation/structure/spacefill'; import { BallAndStickProps } from 'mol-geo/representation/structure/ball-and-stick'; +import { DistanceRestraintProps } from 'mol-geo/representation/structure/distance-restraint'; const getNextId = idFactory(1) @@ -127,4 +128,11 @@ export namespace BallAndStickEntity { export function ofRepr(ctx: StateContext, repr: StructureRepresentation<BallAndStickProps>): BallAndStickEntity { return StateEntity.create(ctx, 'ballandstick', repr ) } +} + +export type DistanceRestraintEntity = StateEntity<StructureRepresentation<DistanceRestraintProps>, 'distancerestraint'> +export namespace DistanceRestraintEntity { + export function ofRepr(ctx: StateContext, repr: StructureRepresentation<DistanceRestraintProps>): DistanceRestraintEntity { + return StateEntity.create(ctx, 'distancerestraint', repr ) + } } \ No newline at end of file diff --git a/src/mol-view/state/transform.ts b/src/mol-view/state/transform.ts index 499f564a8..13c6d2c8f 100644 --- a/src/mol-view/state/transform.ts +++ b/src/mol-view/state/transform.ts @@ -5,13 +5,14 @@ */ import CIF from 'mol-io/reader/cif' -import { FileEntity, DataEntity, UrlEntity, CifEntity, MmcifEntity, ModelEntity, StructureEntity, SpacefillEntity, AnyEntity, NullEntity, BallAndStickEntity } from './entity'; +import { FileEntity, DataEntity, UrlEntity, CifEntity, MmcifEntity, ModelEntity, StructureEntity, SpacefillEntity, AnyEntity, NullEntity, BallAndStickEntity, DistanceRestraintEntity } from './entity'; import { Model, Structure, Format } from 'mol-model/structure'; import { StateContext } from './context'; import StructureSymmetry from 'mol-model/structure/structure/symmetry'; import { SpacefillProps, SpacefillRepresentation } from 'mol-geo/representation/structure/spacefill'; import { BallAndStickProps, BallAndStickRepresentation } from 'mol-geo/representation/structure/ball-and-stick'; +import { DistanceRestraintRepresentation, DistanceRestraintProps } from 'mol-geo/representation/structure/distance-restraint'; type transformer<I extends AnyEntity, O extends AnyEntity, P extends {}> = (ctx: StateContext, inputEntity: I, props?: P) => Promise<O> @@ -85,11 +86,11 @@ export const ModelToStructure: ModelToStructure = StateTransform.create('model', }) export type StructureCenter = StateTransform<StructureEntity, NullEntity, {}> - export const StructureCenter: StructureCenter = StateTransform.create('structure', 'null', 'structure-center', - async function (ctx: StateContext, structureEntity: StructureEntity) { - ctx.viewer.center(structureEntity.value.boundary.sphere.center) - return NullEntity - }) +export const StructureCenter: StructureCenter = StateTransform.create('structure', 'null', 'structure-center', + async function (ctx: StateContext, structureEntity: StructureEntity) { + ctx.viewer.center(structureEntity.value.boundary.sphere.center) + return NullEntity + }) export type StructureToSpacefill = StateTransform<StructureEntity, SpacefillEntity, SpacefillProps> export const StructureToSpacefill: StructureToSpacefill = StateTransform.create('structure', 'spacefill', 'structure-to-spacefill', @@ -103,15 +104,26 @@ export const StructureToSpacefill: StructureToSpacefill = StateTransform.create( }) export type StructureToBallAndStick = StateTransform<StructureEntity, BallAndStickEntity, BallAndStickProps> - export const StructureToBallAndStick: StructureToBallAndStick = StateTransform.create('structure', 'ballandstick', 'structure-to-ballandstick', - async function (ctx: StateContext, structureEntity: StructureEntity, props: BallAndStickProps = {}) { - const ballAndStickRepr = BallAndStickRepresentation() - await ballAndStickRepr.create(structureEntity.value, props).run(ctx.log) - ctx.viewer.add(ballAndStickRepr) - ctx.viewer.requestDraw() - console.log('stats', ctx.viewer.stats) - return BallAndStickEntity.ofRepr(ctx, ballAndStickRepr) - }) +export const StructureToBallAndStick: StructureToBallAndStick = StateTransform.create('structure', 'ballandstick', 'structure-to-ballandstick', + async function (ctx: StateContext, structureEntity: StructureEntity, props: BallAndStickProps = {}) { + const ballAndStickRepr = BallAndStickRepresentation() + await ballAndStickRepr.create(structureEntity.value, props).run(ctx.log) + ctx.viewer.add(ballAndStickRepr) + ctx.viewer.requestDraw() + console.log('stats', ctx.viewer.stats) + return BallAndStickEntity.ofRepr(ctx, ballAndStickRepr) + }) + +export type StructureToDistanceRestraint = StateTransform<StructureEntity, DistanceRestraintEntity, DistanceRestraintProps> +export const StructureToDistanceRestraint: StructureToDistanceRestraint = StateTransform.create('structure', 'distancerestraint', 'structure-to-distancerestraint', + async function (ctx: StateContext, structureEntity: StructureEntity, props: DistanceRestraintProps = {}) { + const distanceRestraintRepr = DistanceRestraintRepresentation() + await distanceRestraintRepr.create(structureEntity.value, props).run(ctx.log) + ctx.viewer.add(distanceRestraintRepr) + ctx.viewer.requestDraw() + console.log('stats', ctx.viewer.stats) + return DistanceRestraintEntity.ofRepr(ctx, distanceRestraintRepr) + }) export type SpacefillUpdate = StateTransform<SpacefillEntity, NullEntity, SpacefillProps> export const SpacefillUpdate: SpacefillUpdate = StateTransform.create('spacefill', 'null', 'spacefill-update', @@ -125,15 +137,26 @@ export const SpacefillUpdate: SpacefillUpdate = StateTransform.create('spacefill }) export type BallAndStickUpdate = StateTransform<BallAndStickEntity, NullEntity, BallAndStickProps> - export const BallAndStickUpdate: BallAndStickUpdate = StateTransform.create('ballandstick', 'null', 'ballandstick-update', - async function (ctx: StateContext, ballAndStickEntity: BallAndStickEntity, props: BallAndStickProps = {}) { - const ballAndStickRepr = ballAndStickEntity.value - await ballAndStickRepr.update(props).run(ctx.log) - ctx.viewer.add(ballAndStickRepr) - ctx.viewer.requestDraw() - console.log('stats', ctx.viewer.stats) - return NullEntity - }) +export const BallAndStickUpdate: BallAndStickUpdate = StateTransform.create('ballandstick', 'null', 'ballandstick-update', + async function (ctx: StateContext, ballAndStickEntity: BallAndStickEntity, props: BallAndStickProps = {}) { + const ballAndStickRepr = ballAndStickEntity.value + await ballAndStickRepr.update(props).run(ctx.log) + ctx.viewer.add(ballAndStickRepr) + ctx.viewer.requestDraw() + console.log('stats', ctx.viewer.stats) + return NullEntity + }) + +export type DistanceRestraintUpdate = StateTransform<DistanceRestraintEntity, NullEntity, DistanceRestraintProps> +export const DistanceRestraintUpdate: DistanceRestraintUpdate = StateTransform.create('distancerestraint', 'null', 'distancerestraint-update', + async function (ctx: StateContext, distanceRestraintEntity: DistanceRestraintEntity, props: DistanceRestraintProps = {}) { + const distanceRestraintRepr = distanceRestraintEntity.value + await distanceRestraintRepr.update(props).run(ctx.log) + ctx.viewer.add(distanceRestraintRepr) + ctx.viewer.requestDraw() + console.log('stats', ctx.viewer.stats) + return NullEntity + }) // composed -- GitLab