diff --git a/src/extensions/dnatco/README.md b/src/extensions/dnatco/README.md new file mode 100644 index 0000000000000000000000000000000000000000..6b5bc05258c1bc2afa4771dfc04acf619280f3fa --- /dev/null +++ b/src/extensions/dnatco/README.md @@ -0,0 +1,10 @@ +## DNATCO Extensions + +### Confal Pyramids + +The Confal Pyramids extensions displays tetrahedron-like pyramids. These pyramids are a simple visual representation of nucleotide conformer classes that can be assigned to individual steps in nucleic acid structures. + +For more information, see: +* [Černý et al., Nucleic Acids Research, 44, W284 (2016)](http://dx.doi.org/10.1093/nar/gkw381) +* [Schneider et al., Acta Cryst D, 74, 52-64 (2018)](http://dx.doi.org/10.1107/S2059798318000050) +* [Schneider et al., Genes, 8(10), 278, (2017)](http://dx.doi.org/10.3390/genes8100278) diff --git a/src/extensions/dnatco/confal-pyramids/behavior.ts b/src/extensions/dnatco/confal-pyramids/behavior.ts new file mode 100644 index 0000000000000000000000000000000000000000..23df06b368487d7338510c1bfef8cbf591667795 --- /dev/null +++ b/src/extensions/dnatco/confal-pyramids/behavior.ts @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Michal Malý <michal.maly@ibt.cas.cz> + * @author Jiří Černý <jiri.cerny@ibt.cas.cz> + */ + +import { ConfalPyramidsColorThemeProvider } from './color'; +import { ConfalPyramids, ConfalPyramidsProvider } from './property'; +import { ConfalPyramidsRepresentationProvider } from './representation'; +import { Loci } from '../../../mol-model/loci'; +import { PluginBehavior } from '../../../mol-plugin/behavior/behavior'; +import { StructureRepresentationPresetProvider, PresetStructureRepresentations } from '../../../mol-plugin-state/builder/structure/representation-preset'; +import { StateObjectRef } from '../../../mol-state'; +import { Task } from '../../../mol-task'; +import { ParamDefinition as PD } from '../../../mol-util/param-definition'; + +export const DnatcoConfalPyramidsPreset = StructureRepresentationPresetProvider({ + id: 'preset-structure-representation-confal-pyramids', + display: { + name: 'Confal Pyramids', + description: 'Schematic depiction of conformer class and confal value.', + }, + isApplicable(a) { + return a.data.models.length >= 1 && a.data.models.some(m => ConfalPyramids.isApplicable(m)); + }, + params: () => StructureRepresentationPresetProvider.CommonParams, + async apply(ref, params, plugin) { + const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref); + const model = structureCell?.obj?.data.model; + if (!structureCell || !model) return {}; + + await plugin.runTask(Task.create('Confal Pyramids', async runtime => { + await ConfalPyramidsProvider.attach({ runtime, assetManager: plugin.managers.asset }, model); + })); + + const { components, representations } = await PresetStructureRepresentations.auto.apply(ref, { ...params }, plugin); + + const pyramids = await plugin.builders.structure.tryCreateComponentStatic(structureCell, 'nucleic', { label: 'Confal Pyramids' }); + const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params); + + let pyramidsRepr; + if (representations) + pyramidsRepr = builder.buildRepresentation(update, pyramids, { type: ConfalPyramidsRepresentationProvider, typeParams, color: ConfalPyramidsColorThemeProvider }, { tag: 'confal-pyramdis' } ); + + await update.commit({ revertOnError: true }); + return { components: { ...components, pyramids }, representations: { ...representations, pyramidsRepr } }; + } +}); + +export const DnatcoConfalPyramids = PluginBehavior.create<{ autoAttach: boolean, showToolTip: boolean }>({ + name: 'dnatco-confal-pyramids-prop', + category: 'custom-props', + display: { + name: 'Confal Pyramids', + description: 'Schematic depiction of conformer class and confal value.', + }, + ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean, showToolTip: boolean }> { + + private provider = ConfalPyramidsProvider; + + private labelConfalPyramids = { + label: (loci: Loci): string | undefined => { + if (!this.params.showToolTip) return void 0; + + /* TODO: Implement this */ + return void 0; + } + } + + register(): void { + this.ctx.customModelProperties.register(this.provider, this.params.autoAttach); + this.ctx.managers.lociLabels.addProvider(this.labelConfalPyramids); + + this.ctx.representation.structure.themes.colorThemeRegistry.add(ConfalPyramidsColorThemeProvider); + this.ctx.representation.structure.registry.add(ConfalPyramidsRepresentationProvider); + + this.ctx.builders.structure.representation.registerPreset(DnatcoConfalPyramidsPreset); + } + + update(p: { autoAttach: boolean, showToolTip: boolean }) { + const updated = this.params.autoAttach !== p.autoAttach; + this.params.autoAttach = p.autoAttach; + this.params.showToolTip = p.showToolTip; + this.ctx.customModelProperties.setDefaultAutoAttach(this.provider.descriptor.name, this.params.autoAttach); + return updated; + } + + unregister() { + this.ctx.customModelProperties.unregister(ConfalPyramidsProvider.descriptor.name); + this.ctx.managers.lociLabels.removeProvider(this.labelConfalPyramids); + + this.ctx.representation.structure.registry.remove(ConfalPyramidsRepresentationProvider); + this.ctx.representation.structure.themes.colorThemeRegistry.remove(ConfalPyramidsColorThemeProvider); + + this.ctx.builders.structure.representation.unregisterPreset(DnatcoConfalPyramidsPreset); + } + }, + params: () => ({ + autoAttach: PD.Boolean(true), + showToolTip: PD.Boolean(true) + }) +}); diff --git a/src/extensions/dnatco/confal-pyramids/color.ts b/src/extensions/dnatco/confal-pyramids/color.ts new file mode 100644 index 0000000000000000000000000000000000000000..ededefa0d3f44639c9503dd1cc40edfb8953568c --- /dev/null +++ b/src/extensions/dnatco/confal-pyramids/color.ts @@ -0,0 +1,178 @@ +/** + * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Michal Malý <michal.maly@ibt.cas.cz> + * @author Jiří Černý <jiri.cerny@ibt.cas.cz> + */ + +import { ConfalPyramids, ConfalPyramidsProvider } from './property'; +import { ConfalPyramidsTypes as CPT } from './types'; +import { Location } from '../../../mol-model/location'; +import { CustomProperty } from '../../../mol-model-props/common/custom-property'; +import { ColorTheme } from '../../../mol-theme/color'; +import { ThemeDataContext } from '../../../mol-theme/theme'; +import { Color } from '../../../mol-util/color'; +import { ParamDefinition as PD } from '../../../mol-util/param-definition'; + +const DefaultColor = Color(0xCCCCCC); +const Description = 'Assigns colors to confal pyramids'; +const ErrorColor = Color(0xFFA10A); + +type ConformerClasses = 'A' | 'B' | 'BII' | 'miB' | 'Z' | 'IC' | 'OPN' | 'SYN' | 'N'; + +const ColorMapping: ReadonlyMap<ConformerClasses, Color> = new Map([ + ['A', Color(0xFFC1C1)], + ['B', Color(0xC8CFFF)], + ['BII', Color(0x0059DA)], + ['miB', Color(0x3BE8FB)], + ['Z', Color(0x01F60E)], + ['IC', Color(0xFA5CFB)], + ['OPN', Color(0xE90000)], + ['SYN', Color(0xFFFF01)], + ['N', Color(0xF2F2F2)], +]); + +const NtCToClasses: ReadonlyMap<string, [ConformerClasses, ConformerClasses]> = new Map([ + ['NANT', ['N', 'N']], + ['AA00', ['A', 'A']], + ['AA02', ['A', 'A']], + ['AA03', ['A', 'A']], + ['AA04', ['A', 'A']], + ['AA08', ['A', 'A']], + ['AA09', ['A', 'A']], + ['AA01', ['A', 'A']], + ['AA05', ['A', 'A']], + ['AA06', ['A', 'A']], + ['AA10', ['A', 'A']], + ['AA11', ['A', 'A']], + ['AA07', ['A', 'A']], + ['AA12', ['A', 'A']], + ['AA13', ['A', 'A']], + ['AB01', ['A', 'B']], + ['AB02', ['A', 'B']], + ['AB03', ['A', 'B']], + ['AB04', ['A', 'B']], + ['AB05', ['A', 'B']], + ['BA01', ['B', 'A']], + ['BA05', ['B', 'A']], + ['BA09', ['B', 'A']], + ['BA08', ['BII', 'A']], + ['BA10', ['B', 'A']], + ['BA13', ['BII', 'A']], + ['BA16', ['BII', 'A']], + ['BA17', ['BII', 'A']], + ['BB00', ['B', 'B']], + ['BB01', ['B', 'B']], + ['BB17', ['B', 'B']], + ['BB02', ['B', 'B']], + ['BB03', ['B', 'B']], + ['BB11', ['B', 'B']], + ['BB16', ['B', 'B']], + ['BB04', ['B', 'BII']], + ['BB05', ['B', 'BII']], + ['BB07', ['BII', 'BII']], + ['BB08', ['BII', 'BII']], + ['BB10', ['miB', 'miB']], + ['BB12', ['miB', 'miB']], + ['BB13', ['miB', 'miB']], + ['BB14', ['miB', 'miB']], + ['BB15', ['miB', 'miB']], + ['BB20', ['miB', 'miB']], + ['IC01', ['IC', 'IC']], + ['IC02', ['IC', 'IC']], + ['IC03', ['IC', 'IC']], + ['IC04', ['IC', 'IC']], + ['IC05', ['IC', 'IC']], + ['IC06', ['IC', 'IC']], + ['IC07', ['IC', 'IC']], + ['OP01', ['OPN', 'OPN']], + ['OP02', ['OPN', 'OPN']], + ['OP03', ['OPN', 'OPN']], + ['OP04', ['OPN', 'OPN']], + ['OP05', ['OPN', 'OPN']], + ['OP06', ['OPN', 'OPN']], + ['OP07', ['OPN', 'OPN']], + ['OP08', ['OPN', 'OPN']], + ['OP09', ['OPN', 'OPN']], + ['OP10', ['OPN', 'OPN']], + ['OP11', ['OPN', 'OPN']], + ['OP12', ['OPN', 'OPN']], + ['OP13', ['OPN', 'OPN']], + ['OP14', ['OPN', 'OPN']], + ['OP15', ['OPN', 'OPN']], + ['OP16', ['OPN', 'OPN']], + ['OP17', ['OPN', 'OPN']], + ['OP18', ['OPN', 'OPN']], + ['OP19', ['OPN', 'OPN']], + ['OP20', ['OPN', 'OPN']], + ['OP21', ['OPN', 'OPN']], + ['OP22', ['OPN', 'OPN']], + ['OP23', ['OPN', 'OPN']], + ['OP24', ['OPN', 'OPN']], + ['OP25', ['OPN', 'OPN']], + ['OP26', ['OPN', 'OPN']], + ['OP27', ['OPN', 'OPN']], + ['OP28', ['OPN', 'OPN']], + ['OP29', ['OPN', 'OPN']], + ['OP30', ['OPN', 'OPN']], + ['OP31', ['OPN', 'OPN']], + ['OPS1', ['OPN', 'OPN']], + ['OP1S', ['OPN', 'SYN']], + ['AAS1', ['SYN', 'A']], + ['AB1S', ['A', 'SYN']], + ['AB2S', ['A', 'SYN']], + ['BB1S', ['B', 'SYN']], + ['BB2S', ['B', 'SYN']], + ['BBS1', ['SYN', 'B']], + ['ZZ01', ['Z', 'Z']], + ['ZZ02', ['Z', 'Z']], + ['ZZ1S', ['Z', 'SYN']], + ['ZZ2S', ['Z', 'SYN']], + ['ZZS1', ['SYN', 'Z']], + ['ZZS2', ['SYN', 'Z']], +]); + +function getConformerColor(ntc: string, useLower: boolean): Color { + const item = NtCToClasses.get(ntc); + if (!item) return ErrorColor; + return ColorMapping.get(useLower ? item[1] : item[0]) ?? ErrorColor; +} + +export const ConfalPyramidsColorThemeParams = {}; +export type ConfalPyramidsColorThemeParams = typeof ConfalPyramidsColorThemeParams +export function getConfalPyramidsColorThemeParams(ctx: ThemeDataContext) { + return ConfalPyramidsColorThemeParams; // TODO return copy +} + +export function ConfalPyramidsColorTheme(ctx: ThemeDataContext, props: PD.Values<ConfalPyramidsColorThemeParams>): ColorTheme<ConfalPyramidsColorThemeParams> { + function color(location: Location, isSecondary: boolean): Color { + if (CPT.isLocation(location)) { + const { pyramid, isLower } = location.data; + return getConformerColor(pyramid.NtC, isLower); + } + + return DefaultColor; + } + + return { + factory: ConfalPyramidsColorTheme, + granularity: 'group', + color, + props, + description: Description, + }; +} + +export const ConfalPyramidsColorThemeProvider: ColorTheme.Provider<ConfalPyramidsColorThemeParams, 'confal-pyramids'> = { + name: 'confal-pyramids', + label: 'Confal Pyramids', + category: ColorTheme.Category.Residue, + factory: ConfalPyramidsColorTheme, + getParams: getConfalPyramidsColorThemeParams, + defaultValues: PD.getDefaultValues(ConfalPyramidsColorThemeParams), + isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && ctx.structure.models.some(m => ConfalPyramids.isApplicable(m)), + ensureCustomProperties: { + attach: (ctx: CustomProperty.Context, data: ThemeDataContext) => data.structure ? ConfalPyramidsProvider.attach(ctx, data.structure.models[0], void 0, true) : Promise.resolve(), + detach: (data) => data.structure && data.structure.models[0].customProperties.reference(ConfalPyramidsProvider.descriptor, false) + } +}; diff --git a/src/extensions/dnatco/confal-pyramids/property.ts b/src/extensions/dnatco/confal-pyramids/property.ts new file mode 100644 index 0000000000000000000000000000000000000000..00dcf95cc5787b736bca1ad6016d664c3bb188db --- /dev/null +++ b/src/extensions/dnatco/confal-pyramids/property.ts @@ -0,0 +1,172 @@ +/** + * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Michal Malý <michal.maly@ibt.cas.cz> + * @author Jiří Černý <jiri.cerny@ibt.cas.cz> + */ + +import { ConfalPyramidsTypes as CPT } from './types'; +import { Column, Table } from '../../../mol-data/db'; +import { toTable } from '../../../mol-io/reader/cif/schema'; +import { CustomPropertyDescriptor } from '../../../mol-model/custom-property'; +import { Model } from '../../../mol-model/structure'; +import { CustomProperty } from '../../../mol-model-props/common/custom-property'; +import { CustomModelProperty } from '../../../mol-model-props/common/custom-model-property'; +import { PropertyWrapper } from '../../../mol-model-props/common/wrapper'; +import { ParamDefinition as PD } from '../../../mol-util/param-definition'; +import { MmcifFormat } from '../../../mol-model-formats/structure/mmcif'; + +export type ConfalPyramids = PropertyWrapper<CPT.PyramidsData | undefined >; + +export namespace ConfalPyramids { + export const Schema = { + ndb_struct_ntc_step: { + id: Column.Schema.int, + name: Column.Schema.str, + PDB_model_number: Column.Schema.int, + label_entity_id_1: Column.Schema.int, + label_asym_id_1: Column.Schema.str, + label_seq_id_1: Column.Schema.int, + label_comp_id_1: Column.Schema.str, + label_alt_id_1: Column.Schema.str, + label_entity_id_2: Column.Schema.int, + label_asym_id_2: Column.Schema.str, + label_seq_id_2: Column.Schema.int, + label_comp_id_2: Column.Schema.str, + label_alt_id_2: Column.Schema.str, + auth_asym_id_1: Column.Schema.str, + auth_seq_id_1: Column.Schema.int, + auth_asym_id_2: Column.Schema.str, + auth_seq_id_2: Column.Schema.int, + PDB_ins_code_1: Column.Schema.str, + PDB_ins_code_2: Column.Schema.str, + }, + ndb_struct_ntc_step_summary: { + step_id: Column.Schema.int, + assigned_CANA: Column.Schema.str, + assigned_NtC: Column.Schema.str, + confal_score: Column.Schema.int, + euclidean_distance_NtC_ideal: Column.Schema.float, + cartesian_rmsd_closest_NtC_representative: Column.Schema.float, + closest_CANA: Column.Schema.str, + closest_NtC: Column.Schema.str, + closest_step_golden: Column.Schema.str + } + }; + export type Schema = typeof Schema; + + export async function fromCif(ctx: CustomProperty.Context, model: Model, props: ConfalPyramidsProps): Promise<CustomProperty.Data<ConfalPyramids>> { + const info = PropertyWrapper.createInfo(); + const data = getCifData(model); + if (data === undefined) return { value: { info, data: undefined } }; + + const fromCif = createPyramidsFromCif(model, data.steps, data.stepsSummary); + return { value: { info, data: fromCif } }; + } + + function getCifData(model: Model) { + if (!MmcifFormat.is(model.sourceData)) throw new Error('Data format must be mmCIF'); + if (!hasNdbStructNtcCategories(model)) return undefined; + return { + steps: toTable(Schema.ndb_struct_ntc_step, model.sourceData.data.frame.categories.ndb_struct_ntc_step), + stepsSummary: toTable(Schema.ndb_struct_ntc_step_summary, model.sourceData.data.frame.categories.ndb_struct_ntc_step_summary) + }; + } + + function hasNdbStructNtcCategories(model: Model): boolean { + if (!MmcifFormat.is(model.sourceData)) throw new Error('Data format must be mmCIF'); + const names = (model.sourceData).data.frame.categoryNames; + return names.includes('ndb_struct_ntc_step') && names.includes('ndb_struct_ntc_step_summary'); + } + + export function isApplicable(model?: Model): boolean { + return !!model && hasNdbStructNtcCategories(model); + } +} + +export const ConfalPyramidsParams = {}; +export type ConfalPyramidsParams = typeof ConfalPyramidsParams; +export type ConfalPyramidsProps = PD.Values<ConfalPyramidsParams>; + +export const ConfalPyramidsProvider: CustomModelProperty.Provider<ConfalPyramidsParams, ConfalPyramids> = CustomModelProperty.createProvider({ + label: 'Confal Pyramids', + descriptor: CustomPropertyDescriptor({ + name: 'confal_pyramids', + }), + type: 'static', + defaultParams: ConfalPyramidsParams, + getParams: (data: Model) => ConfalPyramidsParams, + isApplicable: (data: Model) => ConfalPyramids.isApplicable(data), + obtain: async (ctx: CustomProperty.Context, data: Model, props: Partial<ConfalPyramidsProps>) => { + const p = { ...PD.getDefaultValues(ConfalPyramidsParams), ...props }; + return ConfalPyramids.fromCif(ctx, data, p); + } +}); + +type StepsSummaryTable = Table<typeof ConfalPyramids.Schema.ndb_struct_ntc_step_summary>; + +function createPyramidsFromCif(model: Model, + steps: Table<typeof ConfalPyramids.Schema.ndb_struct_ntc_step>, + stepsSummary: StepsSummaryTable): CPT.PyramidsData { + const pyramids = new Array<CPT.Pyramid>(); + const names = new Map<string, number>(); + const locations = new Array<CPT.Location>(); + let hasMultipleModels = false; + + const { + id, PDB_model_number, name, + auth_asym_id_1, auth_seq_id_1, label_comp_id_1, label_alt_id_1, PDB_ins_code_1, + auth_asym_id_2, auth_seq_id_2, label_comp_id_2, label_alt_id_2, PDB_ins_code_2, + _rowCount } = steps; + + if (_rowCount !== stepsSummary._rowCount) throw new Error('Inconsistent mmCIF data'); + + for (let i = 0; i < _rowCount; i++) { + const model_num = PDB_model_number.value(i); + if (model_num !== model.modelNum) { + hasMultipleModels = true; + continue; // We are only interested in data for the current model + } + + const { _NtC, _confal_score } = getNtCAndConfalScore(id.value(i), i, stepsSummary); + + const pyramid = { + PDB_model_number: model_num, + name: name.value(i), + auth_asym_id_1: auth_asym_id_1.value(i), + auth_seq_id_1: auth_seq_id_1.value(i), + label_comp_id_1: label_comp_id_1.value(i), + label_alt_id_1: label_alt_id_1.value(i), + PDB_ins_code_1: PDB_ins_code_1.value(i), + auth_asym_id_2: auth_asym_id_2.value(i), + auth_seq_id_2: auth_seq_id_2.value(i), + label_comp_id_2: label_comp_id_2.value(i), + label_alt_id_2: label_alt_id_2.value(i), + PDB_ins_code_2: PDB_ins_code_2.value(i), + confal_score: _confal_score, + NtC: _NtC + }; + + pyramids.push(pyramid); + names.set(pyramid.name, pyramids.length - 1); + + locations.push(CPT.Location(pyramid, false)); + locations.push(CPT.Location(pyramid, true)); + } + + return { pyramids, names, locations, hasMultipleModels }; +} + +function getNtCAndConfalScore(id: number, i: number, stepsSummary: StepsSummaryTable) { + const { step_id, confal_score, assigned_NtC } = stepsSummary; + + // Assume that step_ids in ntc_step_summary are in the same order as steps in ntc_step + for (let j = i; j < stepsSummary._rowCount; j++) { + if (id === step_id.value(j)) return { _NtC: assigned_NtC.value(j), _confal_score: confal_score.value(j) }; + } + // Safety net for cases where the previous assumption is not met + for (let j = 0; j < i; j++) { + if (id === step_id.value(j)) return { _NtC: assigned_NtC.value(j), _confal_score: confal_score.value(j) }; + } + throw new Error('Inconsistent mmCIF data'); +} diff --git a/src/extensions/dnatco/confal-pyramids/representation.ts b/src/extensions/dnatco/confal-pyramids/representation.ts new file mode 100644 index 0000000000000000000000000000000000000000..32074a3d2e32cd6c1712eaf981e55952bbff42d9 --- /dev/null +++ b/src/extensions/dnatco/confal-pyramids/representation.ts @@ -0,0 +1,186 @@ +/** + * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Michal Malý <michal.maly@ibt.cas.cz> + * @author Jiří Černý <jiri.cerny@ibt.cas.cz> + */ + +import { ConfalPyramids, ConfalPyramidsProvider } from './property'; +import { ConfalPyramidsUtil } from './util'; +import { ConfalPyramidsTypes as CPT } from './types'; +import { Interval } from '../../../mol-data/int'; +import { Mesh } from '../../../mol-geo/geometry/mesh/mesh'; +import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder'; +import { PickingId } from '../../../mol-geo/geometry/picking'; +import { PrimitiveBuilder } from '../../../mol-geo/primitive/primitive'; +import { LocationIterator } from '../../../mol-geo/util/location-iterator'; +import { Mat4, Vec3 } from '../../../mol-math/linear-algebra'; +import { EmptyLoci, Loci } from '../../../mol-model/loci'; +import { Structure, StructureProperties, Unit } from '../../../mol-model/structure'; +import { CustomProperty } from '../../../mol-model-props/common/custom-property'; +import { Representation, RepresentationContext, RepresentationParamsGetter } from '../../../mol-repr/representation'; +import { StructureRepresentation, StructureRepresentationProvider, StructureRepresentationStateBuilder, UnitsRepresentation } from '../../../mol-repr/structure/representation'; +import { StructureGroup, UnitsMeshParams, UnitsMeshVisual, UnitsVisual } from '../../../mol-repr/structure/units-visual'; +import { VisualUpdateState } from '../../../mol-repr/util'; +import { VisualContext } from '../../../mol-repr/visual'; +import { getAltResidueLociFromId } from '../../../mol-repr/structure/visual/util/common'; +import { ParamDefinition as PD } from '../../../mol-util/param-definition'; +import { Theme, ThemeRegistryContext } from '../../../mol-theme/theme'; +import { NullLocation } from '../../../mol-model/location'; + +const t = Mat4.identity(); +const w = Vec3.zero(); +const mp = Vec3.zero(); + +function calcMidpoint(mp: Vec3, v: Vec3, w: Vec3) { + Vec3.sub(mp, v, w); + Vec3.scale(mp, mp, 0.5); + Vec3.add(mp, mp, w); +} + +function shiftVertex(vec: Vec3, ref: Vec3, scale: number) { + Vec3.sub(w, vec, ref); + Vec3.scale(w, w, scale); + Vec3.add(vec, vec, w); +} + +const ConfalPyramidsMeshParams = { + ...UnitsMeshParams +}; +type ConfalPyramidsMeshParams = typeof ConfalPyramidsMeshParams; + +function createConfalPyramidsIterator(structureGroup: StructureGroup): LocationIterator { + const { structure, group } = structureGroup; + const instanceCount = group.units.length; + + const prop = ConfalPyramidsProvider.get(structure.model).value; + if (prop === undefined || prop.data === undefined) { + return LocationIterator(0, 1, () => NullLocation); + } + + const { locations } = prop.data; + + const getLocation = (groupIndex: number, instanceIndex: number) => { + if (locations.length <= groupIndex) return NullLocation; + return locations[groupIndex]; + }; + return LocationIterator(locations.length, instanceCount, getLocation); +} + +function createConfalPyramidsMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PD.Values<ConfalPyramidsMeshParams>, mesh?: Mesh) { + if (!Unit.isAtomic(unit)) return Mesh.createEmpty(mesh); + + const prop = ConfalPyramidsProvider.get(structure.model).value; + if (prop === undefined || prop.data === undefined) return Mesh.createEmpty(mesh); + + const { pyramids } = prop.data; + if (pyramids.length === 0) return Mesh.createEmpty(mesh); + + const mb = MeshBuilder.createState(512, 512, mesh); + + const handler = (pyramid: CPT.Pyramid, first: ConfalPyramidsUtil.FirstResidueAtoms, second: ConfalPyramidsUtil.SecondResidueAtoms, firsLocIndex: number, secondLocIndex: number) => { + if (firsLocIndex === -1 || secondLocIndex === -1) + throw new Error('Invalid location index'); + + const scale = (pyramid.confal_score - 20.0) / 100.0; + const O3 = first.O3.pos; + const OP1 = second.OP1.pos; const OP2 = second.OP2.pos; const O5 = second.O5.pos; const P = second.P.pos; + + shiftVertex(O3, P, scale); + shiftVertex(OP1, P, scale); + shiftVertex(OP2, P, scale); + shiftVertex(O5, P, scale); + calcMidpoint(mp, O3, O5); + + mb.currentGroup = firsLocIndex; + let pb = PrimitiveBuilder(3); + /* Upper part (for first residue in step) */ + pb.add(O3, OP1, OP2); + pb.add(O3, mp, OP1); + pb.add(O3, OP2, mp); + MeshBuilder.addPrimitive(mb, t, pb.getPrimitive()); + + /* Lower part (for second residue in step */ + mb.currentGroup = secondLocIndex; + pb = PrimitiveBuilder(3); + pb.add(mp, O5, OP1); + pb.add(mp, OP2, O5); + pb.add(O5, OP2, OP1); + MeshBuilder.addPrimitive(mb, t, pb.getPrimitive()); + }; + + const walker = new ConfalPyramidsUtil.UnitWalker(structure, unit, handler); + walker.walk(); + + return MeshBuilder.getMesh(mb); +} + +function getConfalPyramidLoci(pickingId: PickingId, structureGroup: StructureGroup, id: number) { + const { groupId, objectId, instanceId } = pickingId; + if (objectId !== id) return EmptyLoci; + + const { structure } = structureGroup; + + const unit = structureGroup.group.units[instanceId]; + if (!Unit.isAtomic(unit)) return EmptyLoci; + + const prop = ConfalPyramidsProvider.get(structure.model).value; + if (prop === undefined || prop.data === undefined) return EmptyLoci; + + const { locations } = prop.data; + + if (locations.length <= groupId) return EmptyLoci; + const altId = StructureProperties.atom.label_alt_id(CPT.toElementLocation(locations[groupId])); + const rI = unit.residueIndex[locations[groupId].element.element]; + + return getAltResidueLociFromId(structure, unit, rI, altId); +} + +function eachConfalPyramid(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) { + return false; // TODO: Implement me +} + +function ConfalPyramidsVisual(materialId: number): UnitsVisual<ConfalPyramidsMeshParams> { + return UnitsMeshVisual<ConfalPyramidsMeshParams>({ + defaultProps: PD.getDefaultValues(ConfalPyramidsMeshParams), + createGeometry: createConfalPyramidsMesh, + createLocationIterator: createConfalPyramidsIterator, + getLoci: getConfalPyramidLoci, + eachLocation: eachConfalPyramid, + setUpdateState: (state: VisualUpdateState, newProps: PD.Values<ConfalPyramidsMeshParams>, currentProps: PD.Values<ConfalPyramidsMeshParams>) => { + } + }, materialId); +} +const ConfalPyramidsVisuals = { + 'confal-pyramids-symbol': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, UnitsMeshParams>) => UnitsRepresentation('Confal Pyramids Symbol Mesh', ctx, getParams, ConfalPyramidsVisual), +}; + +export const ConfalPyramidsParams = { + ...UnitsMeshParams +}; +export type ConfalPyramidsParams = typeof ConfalPyramidsParams; +export function getConfalPyramidsParams(ctx: ThemeRegistryContext, structure: Structure) { + return PD.clone(ConfalPyramidsParams); +} + +export type ConfalPyramidsRepresentation = StructureRepresentation<ConfalPyramidsParams>; +export function ConfalPyramidsRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, ConfalPyramidsParams>): ConfalPyramidsRepresentation { + const repr = Representation.createMulti('Confal Pyramids', ctx, getParams, StructureRepresentationStateBuilder, ConfalPyramidsVisuals as unknown as Representation.Def<Structure, ConfalPyramidsParams>); + return repr; +} + +export const ConfalPyramidsRepresentationProvider = StructureRepresentationProvider({ + name: 'confal-pyramids', + label: 'Confal Pyramids', + description: 'Displays schematic depiction of conformer classes and confal values', + factory: ConfalPyramidsRepresentation, + getParams: getConfalPyramidsParams, + defaultValues: PD.getDefaultValues(ConfalPyramidsParams), + defaultColorTheme: { name: 'confal-pyramids' }, + defaultSizeTheme: { name: 'uniform' }, + isApplicable: (structure: Structure) => structure.models.some(m => ConfalPyramids.isApplicable(m)), + ensureCustomProperties: { + attach: (ctx: CustomProperty.Context, structure: Structure) => ConfalPyramidsProvider.attach(ctx, structure.model, void 0, true), + detach: (data) => ConfalPyramidsProvider.ref(data.model, false), + } +}); diff --git a/src/extensions/dnatco/confal-pyramids/types.ts b/src/extensions/dnatco/confal-pyramids/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..b7a94099885cb2aa5e37094cb6bb100c355b2392 --- /dev/null +++ b/src/extensions/dnatco/confal-pyramids/types.ts @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Michal Malý <michal.maly@ibt.cas.cz> + * @author Jiří Černý <jiri.cerny@ibt.cas.cz> + */ + +import { DataLocation } from '../../../mol-model/location'; +import { ElementIndex, Structure, StructureElement, Unit } from '../../../mol-model/structure'; + +export namespace ConfalPyramidsTypes { + export type Pyramid = { + PDB_model_number: number, + name: string, + auth_asym_id_1: string, + auth_seq_id_1: number, + label_comp_id_1: string, + label_alt_id_1: string, + PDB_ins_code_1: string, + auth_asym_id_2: string, + auth_seq_id_2: number, + label_comp_id_2: string, + label_alt_id_2: string, + PDB_ins_code_2: string, + confal_score: number, + NtC: string + } + + export interface PyramidsData { + pyramids: Array<Pyramid>, + names: Map<string, number>, + locations: Array<Location>, + hasMultipleModels: boolean + } + + export interface LocationData { + readonly pyramid: Pyramid + readonly isLower: boolean; + } + + export interface Element { + structure: Structure; + unit: Unit.Atomic; + element: ElementIndex; + } + + export interface Location extends DataLocation<LocationData, Element> {} + + export function Location(pyramid: Pyramid, isLower: boolean, structure?: Structure, unit?: Unit.Atomic, element?: ElementIndex) { + return DataLocation('pyramid', { pyramid, isLower }, { structure: structure as any, unit: unit as any, element: element as any }); + } + + export function isLocation(x: any): x is Location { + return !!x && x.kind === 'data-location' && x.tag === 'pyramid'; + } + + export function toElementLocation(location: Location) { + return StructureElement.Location.create(location.element.structure, location.element.unit, location.element.element); + } +} diff --git a/src/extensions/dnatco/confal-pyramids/util.ts b/src/extensions/dnatco/confal-pyramids/util.ts new file mode 100644 index 0000000000000000000000000000000000000000..3f4c627a643f6ed9df090ce7a24c01b240e5117d --- /dev/null +++ b/src/extensions/dnatco/confal-pyramids/util.ts @@ -0,0 +1,299 @@ +/** + * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Michal Malý <michal.maly@ibt.cas.cz> + * @author Jiří Černý <jiri.cerny@ibt.cas.cz> + */ + +import { ConfalPyramidsProvider } from './property'; +import { ConfalPyramidsTypes as CPT } from './types'; +import { OrderedSet, Segmentation } from '../../../mol-data/int'; +import { Vec3 } from '../../../mol-math/linear-algebra'; +import { ChainIndex, ElementIndex, ResidueIndex, Structure, StructureElement, StructureProperties, Unit } from '../../../mol-model/structure'; + +export namespace ConfalPyramidsUtil { + type Residue = Segmentation.Segment<ResidueIndex>; + + export type AtomInfo = { + pos: Vec3, + index: ElementIndex, + fakeAltId: string, + }; + + export type FirstResidueAtoms = { + O3: AtomInfo, + }; + + export type SecondResidueAtoms = { + OP1: AtomInfo, + OP2: AtomInfo, + O5: AtomInfo, + P: AtomInfo, + }; + + type ResidueInfo = { + PDB_model_num: number, + asym_id: string, + auth_asym_id: string, + seq_id: number, + auth_seq_id: number, + comp_id: string, + alt_id: string, + ins_code: string, + }; + + export type Handler = (pyramid: CPT.Pyramid, first: FirstResidueAtoms, second: SecondResidueAtoms, firstLocIndex: number, secondLocIndex: number) => void; + + function residueInfoFromLocation(loc: StructureElement.Location): ResidueInfo { + return { + PDB_model_num: StructureProperties.unit.model_num(loc), + asym_id: StructureProperties.chain.label_asym_id(loc), + auth_asym_id: StructureProperties.chain.auth_asym_id(loc), + seq_id: StructureProperties.residue.label_seq_id(loc), + auth_seq_id: StructureProperties.residue.auth_seq_id(loc), + comp_id: StructureProperties.atom.label_comp_id(loc), + alt_id: StructureProperties.atom.label_alt_id(loc), + ins_code: StructureProperties.residue.pdbx_PDB_ins_code(loc) + }; + } + + export function hasMultipleModels(unit: Unit.Atomic): boolean { + const prop = ConfalPyramidsProvider.get(unit.model).value; + if (prop === undefined || prop.data === undefined) throw new Error('No custom properties data'); + return prop.data.hasMultipleModels; + } + + function getPossibleAltIdsIndices(eIFirst: ElementIndex, eILast: ElementIndex, structure: Structure, unit: Unit.Atomic): string[] { + const loc = StructureElement.Location.create(structure, unit, -1 as ElementIndex); + + const uIFirst = OrderedSet.indexOf(unit.elements, eIFirst); + const uILast = OrderedSet.indexOf(unit.elements, eILast); + + const possibleAltIds: string[] = []; + for (let uI = uIFirst; uI <= uILast; uI++) { + loc.element = unit.elements[uI]; + const altId = StructureProperties.atom.label_alt_id(loc); + if (altId !== '' && !possibleAltIds.includes(altId)) possibleAltIds.push(altId); + } + + return possibleAltIds; + } + + function getPossibleAltIdsResidue(residue: Residue, structure: Structure, unit: Unit.Atomic): string[] { + return getPossibleAltIdsIndices(unit.elements[residue.start], unit.elements[residue.end - 1], structure, unit); + } + + class Utility { + protected getPyramidByName(name: string): { pyramid: CPT.Pyramid | undefined, index: number } { + const index = this.data.names.get(name); + if (index === undefined) return { pyramid: undefined, index: -1 }; + + return { pyramid: this.data.pyramids[index], index }; + } + + protected stepToName(entry_id: string, modelNum: number, locFirst: StructureElement.Location, locSecond: StructureElement.Location, fakeAltId_1: string, fakeAltId_2: string) { + const first = residueInfoFromLocation(locFirst); + const second = residueInfoFromLocation(locSecond); + const model_id = this.hasMultipleModels ? `-m${modelNum}` : ''; + const alt_id_1 = fakeAltId_1 !== '' ? `.${fakeAltId_1}` : (first.alt_id.length ? `.${first.alt_id}` : ''); + const alt_id_2 = fakeAltId_2 !== '' ? `.${fakeAltId_2}` : (second.alt_id.length ? `.${second.alt_id}` : ''); + const ins_code_1 = first.ins_code.length ? `.${first.ins_code}` : ''; + const ins_code_2 = second.ins_code.length ? `.${second.ins_code}` : ''; + + return `${entry_id}${model_id}_${first.auth_asym_id}_${first.comp_id}${alt_id_1}_${first.auth_seq_id}${ins_code_1}_${second.comp_id}${alt_id_2}_${second.auth_seq_id}${ins_code_2}`; + } + + constructor(unit: Unit.Atomic) { + const prop = ConfalPyramidsProvider.get(unit.model).value; + if (prop === undefined || prop.data === undefined) throw new Error('No custom properties data'); + + this.data = prop.data; + this.hasMultipleModels = hasMultipleModels(unit); + + this.entryId = unit.model.entryId.toLowerCase(); + this.modelNum = unit.model.modelNum; + } + + protected readonly data: CPT.PyramidsData + protected readonly hasMultipleModels: boolean; + protected readonly entryId: string; + protected readonly modelNum: number; + } + + export class UnitWalker extends Utility { + private getAtomIndices(names: string[], residue: Residue): ElementIndex[] { + let rI = residue.start; + const rILast = residue.end - 1; + const indices: ElementIndex[] = []; + + for (; rI !== rILast; rI++) { + const eI = this.unit.elements[rI]; + const loc = StructureElement.Location.create(this.structure, this.unit, eI); + const thisName = StructureProperties.atom.label_atom_id(loc); + if (names.includes(thisName)) indices.push(eI); + } + + if (indices.length === 0) + throw new Error(`Element ${name} not found on residue ${residue.index}`); + + return indices; + } + + private getAtomPositions(indices: ElementIndex[]): Vec3[] { + const pos = this.unit.conformation.invariantPosition; + const positions: Vec3[] = []; + + for (const eI of indices) { + const v = Vec3.zero(); + pos(eI, v); + positions.push(v); + } + + return positions; + } + + private handleStep(firstAtoms: FirstResidueAtoms[], secondAtoms: SecondResidueAtoms[]) { + const modelNum = this.hasMultipleModels ? this.modelNum : -1; + let ok = false; + + const firstLoc = StructureElement.Location.create(this.structure, this.unit, -1 as ElementIndex); + const secondLoc = StructureElement.Location.create(this.structure, this.unit, -1 as ElementIndex); + for (let i = 0; i < firstAtoms.length; i++) { + const first = firstAtoms[i]; + for (let j = 0; j < secondAtoms.length; j++) { + const second = secondAtoms[j]; + firstLoc.element = first.O3.index; + secondLoc.element = second.OP1.index; + + const name = this.stepToName(this.entryId, modelNum, firstLoc, secondLoc, first.O3.fakeAltId, second.OP1.fakeAltId); + const { pyramid, index } = this.getPyramidByName(name); + if (pyramid !== undefined) { + const setLoc = (loc: CPT.Location, eI: ElementIndex) => { + loc.element.structure = this.structure; + loc.element.unit = this.unit; + loc.element.element = eI; + }; + + const locIndex = index * 2; + setLoc(this.data.locations[locIndex], firstLoc.element); + setLoc(this.data.locations[locIndex + 1], secondLoc.element); + this.handler(pyramid, first, second, locIndex, locIndex + 1); + ok = true; + } + } + } + + if (!ok) throw new Error('Bogus step'); + } + + private processFirstResidue(residue: Residue, possibleAltIds: string[]) { + const indO3 = this.getAtomIndices(['O3\'', 'O3*'], residue); + const posO3 = this.getAtomPositions(indO3); + + const altPos: FirstResidueAtoms[] = [ + { O3: { pos: posO3[0], index: indO3[0], fakeAltId: '' } } + ]; + + for (let i = 1; i < indO3.length; i++) { + altPos.push({ O3: { pos: posO3[i], index: indO3[i], fakeAltId: '' } }); + } + + if (altPos.length === 1 && possibleAltIds.length > 1) { + /* We have some alternate positions on the residue but O3 does not have any - fake them */ + altPos[0].O3.fakeAltId = possibleAltIds[0]; + + for (let i = 1; i < possibleAltIds.length; i++) + altPos.push({ O3: { pos: posO3[0], index: indO3[0], fakeAltId: possibleAltIds[i] } }); + } + + return altPos; + } + + private processSecondResidue(residue: Residue, possibleAltIds: string[]) { + const indOP1 = this.getAtomIndices(['OP1'], residue); + const indOP2 = this.getAtomIndices(['OP2'], residue); + const indO5 = this.getAtomIndices(['O5\'', 'O5*'], residue); + const indP = this.getAtomIndices(['P'], residue); + + const posOP1 = this.getAtomPositions(indOP1); + const posOP2 = this.getAtomPositions(indOP2); + const posO5 = this.getAtomPositions(indO5); + const posP = this.getAtomPositions(indP); + + const infoOP1: AtomInfo[] = []; + /* We use OP1 as "pivotal" atom. There is no specific reason + * to pick OP1, it is as good a choice as any other atom + */ + if (indOP1.length === 1 && possibleAltIds.length > 1) { + /* No altIds on OP1, fake them */ + for (const altId of possibleAltIds) + infoOP1.push({ pos: posOP1[0], index: indOP1[0], fakeAltId: altId }); + } else { + for (let i = 0; i < indOP1.length; i++) + infoOP1.push({ pos: posOP1[i], index: indOP1[i], fakeAltId: '' }); + } + + const mkInfo = (i: number, indices: ElementIndex[], positions: Vec3[], altId: string) => { + if (i >= indices.length) { + const last = indices.length - 1; + return { pos: positions[last], index: indices[last], fakeAltId: altId }; + } + + return { pos: positions[i], index: indices[i], fakeAltId: altId }; + }; + + const altPos: SecondResidueAtoms[] = []; + for (let i = 0; i < infoOP1.length; i++) { + const altId = infoOP1[i].fakeAltId; + + const OP2 = mkInfo(i, indOP2, posOP2, altId); + const O5 = mkInfo(i, indO5, posO5, altId); + const P = mkInfo(i, indP, posP, altId); + + altPos.push({ OP1: infoOP1[i], OP2, O5, P }); + } + + return altPos; + } + + private step(residue: Residue): { firstAtoms: FirstResidueAtoms[], secondAtoms: SecondResidueAtoms[] } { + const firstPossibleAltIds = getPossibleAltIdsResidue(residue, this.structure, this.unit); + const firstAtoms = this.processFirstResidue(residue, firstPossibleAltIds); + + residue = this.residueIt.move(); + + const secondPossibleAltIds = getPossibleAltIdsResidue(residue, this.structure, this.unit); + const secondAtoms = this.processSecondResidue(residue, secondPossibleAltIds); + + return { firstAtoms, secondAtoms }; + } + + walk() { + while (this.chainIt.hasNext) { + this.residueIt.setSegment(this.chainIt.move()); + + let residue = this.residueIt.move(); + while (this.residueIt.hasNext) { + try { + const { firstAtoms, secondAtoms } = this.step(residue); + + this.handleStep(firstAtoms, secondAtoms); + } catch (error) { + /* Skip and move along */ + residue = this.residueIt.move(); + } + } + } + } + + constructor(private structure: Structure, private unit: Unit.Atomic, private handler: Handler) { + super(unit); + + this.chainIt = Segmentation.transientSegments(unit.model.atomicHierarchy.chainAtomSegments, unit.elements); + this.residueIt = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, unit.elements); + } + + private chainIt: Segmentation.SegmentIterator<ChainIndex>; + private residueIt: Segmentation.SegmentIterator<ResidueIndex>; + } +} diff --git a/src/extensions/dnatco/index.ts b/src/extensions/dnatco/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..1be8f0e39ba10650fe31fe539c80d38e89e7a4f2 --- /dev/null +++ b/src/extensions/dnatco/index.ts @@ -0,0 +1,8 @@ +/** + * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Michal Malý <michal.maly@ibt.cas.cz> + * @author Jiří Černý <jiri.cerny@ibt.cas.cz> + */ + +export { DnatcoConfalPyramids } from './confal-pyramids/behavior';