diff --git a/src/mol-geo/geometry/mesh/builder/ellipsoid.ts b/src/mol-geo/geometry/mesh/builder/ellipsoid.ts new file mode 100644 index 0000000000000000000000000000000000000000..453c9025c6430e2d6514cf1bc2e91ea8a64f7bda --- /dev/null +++ b/src/mol-geo/geometry/mesh/builder/ellipsoid.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { Vec3, Mat4 } from '../../../../mol-math/linear-algebra'; +import { MeshBuilder } from '../mesh-builder'; +import { getSphere } from './sphere'; + +const tmpEllipsoidMat = Mat4.identity() +const tmpVec = Vec3() + +function setEllipsoidMat(m: Mat4, center: Vec3, dirMajor: Vec3, dirMinor: Vec3, radiusScale: Vec3) { + Vec3.add(tmpVec, center, dirMajor) + Mat4.targetTo(m, center, tmpVec, dirMinor) + Mat4.setTranslation(m, center) + return Mat4.scale(m, m, radiusScale) +} + +export function addEllipsoid(state: MeshBuilder.State, center: Vec3, dirMajor: Vec3, dirMinor: Vec3, radiusScale: Vec3, detail: number) { + MeshBuilder.addPrimitive(state, setEllipsoidMat(tmpEllipsoidMat, center, dirMajor, dirMinor, radiusScale), getSphere(detail)) +} \ No newline at end of file diff --git a/src/mol-geo/geometry/mesh/builder/sphere.ts b/src/mol-geo/geometry/mesh/builder/sphere.ts index 2427ed6cd994fcf1054a2835310437cabcacb0f6..041c62e5ff4f14ebf23f310faec76e847a76cf8e 100644 --- a/src/mol-geo/geometry/mesh/builder/sphere.ts +++ b/src/mol-geo/geometry/mesh/builder/sphere.ts @@ -16,7 +16,7 @@ function setSphereMat(m: Mat4, center: Vec3, radius: number) { return Mat4.scaleUniformly(m, Mat4.fromTranslation(m, center), radius) } -function getSphere(detail: number) { +export function getSphere(detail: number) { let sphere = sphereMap.get(detail) if (sphere === undefined) { sphere = Sphere(detail) diff --git a/src/mol-model-formats/structure/mmcif/anisotropic.ts b/src/mol-model-formats/structure/mmcif/anisotropic.ts new file mode 100644 index 0000000000000000000000000000000000000000..dc6bf927665cd4d22203309cd9c60f6eef85e2e6 --- /dev/null +++ b/src/mol-model-formats/structure/mmcif/anisotropic.ts @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { Table } from '../../../mol-data/db'; +import { Model, CustomPropertyDescriptor } from '../../../mol-model/structure'; +import { mmCIF_Schema } from '../../../mol-io/reader/cif/schema/mmcif'; +import { CifWriter } from '../../../mol-io/writer/cif'; + +export interface AtomSiteAnisotrop { + data: Table<AtomSiteAnisotrop.Schema['atom_site_anisotrop']> + /** maps atom_site-index to atom_site_anisotrop-index */ + elementToAnsiotrop: Int32Array +} + +export namespace AtomSiteAnisotrop { + export function getAtomSiteAnisotrop(model: Model) { + if (model.sourceData.kind !== 'mmCIF') return void 0; + const { atom_site_anisotrop } = model.sourceData.data + return Table.ofColumns(Schema.atom_site_anisotrop, atom_site_anisotrop); + } + + export const PropName = '__AtomSiteAnisotrop__'; + export function get(model: Model): AtomSiteAnisotrop | undefined { + if (model._staticPropertyData[PropName]) return model._staticPropertyData[PropName] + if (!model.customProperties.has(Descriptor)) return void 0; + + const data = getAtomSiteAnisotrop(model); + if (!data) return void 0; + + const prop = { data, elementToAnsiotrop: getElementToAnsiotrop(model, data) } + set(model, prop) + + return prop; + } + function set(model: Model, prop: AtomSiteAnisotrop) { + (model._staticPropertyData[PropName] as AtomSiteAnisotrop) = prop; + } + + export const Schema = { atom_site_anisotrop: mmCIF_Schema['atom_site_anisotrop'] }; + export type Schema = typeof Schema + + export const Descriptor: CustomPropertyDescriptor = { + isStatic: true, + name: 'atom_site_anisotrop', + cifExport: { + prefix: '', + categories: [{ + name: 'atom_site_anisotrop', + instance(ctx) { + const atom_site_anisotrop = getAtomSiteAnisotrop(ctx.firstModel); + if (!atom_site_anisotrop) return CifWriter.Category.Empty; + return CifWriter.Category.ofTable(atom_site_anisotrop); + } + }] + } + }; + + function getElementToAnsiotrop(model: Model, data: Table<Schema['atom_site_anisotrop']>) { + const { atomId } = model.atomicConformation + const atomIdToElement = new Int32Array(atomId.rowCount) + atomIdToElement.fill(-1) + for (let i = 0, il = atomId.rowCount; i < il; i++) { + atomIdToElement[atomId.value(i)] = i + } + + const { id } = data + const elementToAnsiotrop = new Int32Array(atomId.rowCount) + elementToAnsiotrop.fill(-1) + for (let i = 0, il = id.rowCount; i < il; ++i) { + const ei = atomIdToElement[id.value(i)] + if (ei !== -1) elementToAnsiotrop[ei] = i + } + + return elementToAnsiotrop + } + + export async function attachFromMmCif(model: Model) { + if (model.customProperties.has(Descriptor)) return true; + if (model.sourceData.kind !== 'mmCIF') return false; + const atomSiteAnisotrop = getAtomSiteAnisotrop(model); + if (!atomSiteAnisotrop || atomSiteAnisotrop._rowCount === 0) return false; + + model.customProperties.add(Descriptor); + return true; + } +} \ No newline at end of file diff --git a/src/mol-model-formats/structure/mmcif/parser.ts b/src/mol-model-formats/structure/mmcif/parser.ts index e0945621e6789f60b8f53ee136e3761d6154ec11..12e6b99cf3552f3d7e90efb34aaed668f3616964 100644 --- a/src/mol-model-formats/structure/mmcif/parser.ts +++ b/src/mol-model-formats/structure/mmcif/parser.ts @@ -29,6 +29,7 @@ import { SaccharideComponentMap, SaccharideComponent, SaccharidesSnfgMap, Saccha import mmCIF_Format = ModelFormat.mmCIF import { memoize1 } from '../../../mol-util/memoize'; import { ElementIndex, EntityIndex } from '../../../mol-model/structure/model'; +import { AtomSiteAnisotrop } from './anisotropic'; export async function _parse_mmCif(format: mmCIF_Format, ctx: RuntimeContext) { const formatData = getFormatData(format) @@ -279,6 +280,7 @@ function createModelIHM(format: mmCIF_Format, data: IHMData, formatData: FormatD function attachProps(model: Model) { ComponentBond.attachFromMmCif(model); StructConn.attachFromMmCif(model); + AtomSiteAnisotrop.attachFromMmCif(model); } function findModelEnd(num: Column<number>, startIndex: number) { diff --git a/src/mol-repr/structure/registry.ts b/src/mol-repr/structure/registry.ts index d2569805ea597e4eb2a000d7f0aa076b142bc99b..ba150d63a0ad3f1036a9aaa49cf2ad7ef26238b4 100644 --- a/src/mol-repr/structure/registry.ts +++ b/src/mol-repr/structure/registry.ts @@ -16,6 +16,7 @@ import { PointRepresentationProvider } from './representation/point'; import { StructureRepresentationState } from './representation'; import { PuttyRepresentationProvider } from './representation/putty'; import { MolecularSurfaceRepresentationProvider } from './representation/molecular-surface'; +import { EllipsoidRepresentationProvider } from './representation/ellipsoid'; export class StructureRepresentationRegistry extends RepresentationRegistry<Structure, StructureRepresentationState> { constructor() { @@ -32,6 +33,7 @@ export const BuiltInStructureRepresentations = { 'ball-and-stick': BallAndStickRepresentationProvider, 'carbohydrate': CarbohydrateRepresentationProvider, 'distance-restraint': DistanceRestraintRepresentationProvider, + 'ellipsoid': EllipsoidRepresentationProvider, 'gaussian-surface': GaussianSurfaceRepresentationProvider, // 'gaussian-volume': GaussianVolumeRepresentationProvider, // TODO disabled for now, needs more work 'molecular-surface': MolecularSurfaceRepresentationProvider, diff --git a/src/mol-repr/structure/representation/ellipsoid.ts b/src/mol-repr/structure/representation/ellipsoid.ts new file mode 100644 index 0000000000000000000000000000000000000000..df74d6a297ad7407e5eb3b9658e713bd697082de --- /dev/null +++ b/src/mol-repr/structure/representation/ellipsoid.ts @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { ParamDefinition as PD } from '../../../mol-util/param-definition'; +import { RepresentationParamsGetter, RepresentationContext, Representation } from '../../../mol-repr/representation'; +import { ThemeRegistryContext } from '../../../mol-theme/theme'; +import { Structure } from '../../../mol-model/structure'; +import { UnitsRepresentation, StructureRepresentation, StructureRepresentationStateBuilder, StructureRepresentationProvider } from '../../../mol-repr/structure/representation'; +import { EllipsoidMeshParams, EllipsoidMeshVisual } from '../visual/ellipsoid-mesh'; +import { UnitKind, UnitKindOptions } from '../../../mol-repr/structure/visual/util/common'; +import { AtomSiteAnisotrop } from '../../../mol-model-formats/structure/mmcif/anisotropic'; + +const EllipsoidVisuals = { + 'ellipsoid-mesh': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, EllipsoidParams>) => UnitsRepresentation('Ellipsoid Mesh', ctx, getParams, EllipsoidMeshVisual), +} + +export const EllipsoidParams = { + ...EllipsoidMeshParams, + unitKinds: PD.MultiSelect<UnitKind>(['atomic'], UnitKindOptions), +} +export type EllipsoidParams = typeof EllipsoidMeshParams +export function getEllipsoidParams(ctx: ThemeRegistryContext, structure: Structure) { + return PD.clone(EllipsoidMeshParams) +} + +export type EllipsoidRepresentation = StructureRepresentation<EllipsoidParams> +export function EllipsoidRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, EllipsoidParams>): EllipsoidRepresentation { + return Representation.createMulti('Ellipsoid', ctx, getParams, StructureRepresentationStateBuilder, EllipsoidVisuals as unknown as Representation.Def<Structure, EllipsoidParams>) +} + +export const EllipsoidRepresentationProvider: StructureRepresentationProvider<EllipsoidParams> = { + label: 'Ellipsoid', + description: 'Displays anisotropic displacement ellipsoids of atomic elements.', + factory: EllipsoidRepresentation, + getParams: getEllipsoidParams, + defaultValues: PD.getDefaultValues(EllipsoidMeshParams), + defaultColorTheme: 'element-symbol', + defaultSizeTheme: 'uniform', + isApplicable: (structure: Structure) => structure.elementCount > 0 && structure.models.some(m => m.customProperties.has(AtomSiteAnisotrop.Descriptor)) +} \ No newline at end of file diff --git a/src/mol-repr/structure/visual/ellipsoid-mesh.ts b/src/mol-repr/structure/visual/ellipsoid-mesh.ts new file mode 100644 index 0000000000000000000000000000000000000000..4c3fc14d6cfde4aa96bc7c16b4531a46c6d1aa10 --- /dev/null +++ b/src/mol-repr/structure/visual/ellipsoid-mesh.ts @@ -0,0 +1,104 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { ParamDefinition as PD } from '../../../mol-util/param-definition'; +import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual } from '../../../mol-repr/structure/units-visual'; +import { ElementIterator, getElementLoci, eachElement } from '../../../mol-repr/structure/visual/util/element'; +import { VisualUpdateState } from '../../../mol-repr/util'; +import { VisualContext } from '../../../mol-repr/visual'; +import { Unit, Structure, StructureElement } from '../../../mol-model/structure'; +import { Theme } from '../../../mol-theme/theme'; +import { Mesh } from '../../../mol-geo/geometry/mesh/mesh'; +import { sphereVertexCount } from '../../../mol-geo/primitive/sphere'; +import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder'; +import { Vec3, Mat3, Tensor } from '../../../mol-math/linear-algebra'; +import { isHydrogen } from '../../../mol-repr/structure/visual/util/common'; +import { addEllipsoid } from '../../../mol-geo/geometry/mesh/builder/ellipsoid'; +import { AtomSiteAnisotrop } from '../../../mol-model-formats/structure/mmcif/anisotropic' + +export const EllipsoidMeshParams = { + ...UnitsMeshParams, + sizeFactor: PD.Numeric(1, { min: 0, max: 10, step: 0.1 }), + detail: PD.Numeric(0, { min: 0, max: 3, step: 1 }), + ignoreHydrogens: PD.Boolean(false), +} +export type EllipsoidMeshParams = typeof EllipsoidMeshParams + +export function EllipsoidMeshVisual(materialId: number): UnitsVisual<EllipsoidMeshParams> { + return UnitsMeshVisual<EllipsoidMeshParams>({ + defaultProps: PD.getDefaultValues(EllipsoidMeshParams), + createGeometry: createEllipsoidMesh, + createLocationIterator: ElementIterator.fromGroup, + getLoci: getElementLoci, + eachLocation: eachElement, + setUpdateState: (state: VisualUpdateState, newProps: PD.Values<EllipsoidMeshParams>, currentProps: PD.Values<EllipsoidMeshParams>) => { + state.createGeometry = ( + newProps.sizeFactor !== currentProps.sizeFactor || + newProps.detail !== currentProps.detail || + newProps.ignoreHydrogens !== currentProps.ignoreHydrogens + ) + } + }, materialId) +} + +// + +export interface EllipsoidMeshProps { + detail: number, + sizeFactor: number, + ignoreHydrogens: boolean, +} + +export function createEllipsoidMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: EllipsoidMeshProps, mesh?: Mesh): Mesh { + const { detail, sizeFactor } = props + + const { elements, model } = unit; + const elementCount = elements.length; + const vertexCount = elementCount * sphereVertexCount(detail) + const builderState = MeshBuilder.createState(vertexCount, vertexCount / 2, mesh) + + const atomSiteAnisotrop = AtomSiteAnisotrop.get(model) + if (!atomSiteAnisotrop) return Mesh.createEmpty(mesh) + + const v = Vec3() + const m = Mat3() + const eigenvalues = Vec3() + const eigenvector1 = Vec3() + const eigenvector2 = Vec3() + const { elementToAnsiotrop, data } = atomSiteAnisotrop + const { U } = data + const space = data._schema.U.space + const pos = unit.conformation.invariantPosition + const l = StructureElement.Location.create() + l.unit = unit + + for (let i = 0; i < elementCount; i++) { + const ei = elements[i] + const ai = elementToAnsiotrop[ei] + if (ai === -1) continue + if (props.ignoreHydrogens && isHydrogen(unit, ei)) continue + + l.element = ei + pos(ei, v) + + builderState.currentGroup = i + Tensor.toMat3(m, space, U.value(ai)) + Mat3.symmtricFromLower(m, m) + Mat3.symmetricEigenvalues(eigenvalues, m) + Mat3.eigenvector(eigenvector1, m, eigenvalues[1]) + Mat3.eigenvector(eigenvector2, m, eigenvalues[2]) + for (let j = 0; j < 3; ++j) { + // show 50% probability surface, needs sqrt as U matrix is in angstrom-squared + // take abs of eigenvalue to avoid reflection + // multiply by given size-factor + eigenvalues[j] = sizeFactor * 1.5958 * Math.sqrt(Math.abs(eigenvalues[j])) + } + + addEllipsoid(builderState, v, eigenvector2, eigenvector1, eigenvalues, detail) + } + + return MeshBuilder.getMesh(builderState) +} \ No newline at end of file