Skip to content
Snippets Groups Projects
Unverified Commit 315401c1 authored by midlik's avatar midlik Committed by GitHub
Browse files

struct-conn extension (#802)

* struct-conn extension toy example

* Minor changes (David's feedback)

* Showing struct_conn visuals

* Removed Interactions visual

* Caching struct_conns

* Removed testing buttons in index.html, updated CHANGELOG

* Addressed most of PR feedback

* Fixed structure node selection, docs

* Addressed feedback round 2
parent 60b5d2d3
No related branches found
No related tags found
No related merge requests found
......@@ -8,6 +8,7 @@ Note that since we don't clearly distinguish between a public and private interf
- Add a uniform color theme for NtC tube that still paints residue and segment dividers in a different color
- Fix bond assignments `struct_conn` records referencing waters
- Add StructConn extension providing functions for inspecting struct_conns
- Fix `PluginState.setSnapshot` triggering unnecessary state updates
- Fix an edge case in the `mol-state`'s `State` when trying to apply a transform to an existing Null object
- Add `SbNcbrPartialCharges` extension for coloring and labeling atoms and residues by partial atomic charges
......
# wwPDB StructConn extension
The STRUCT_CONN category in the mmCIF file format contains details about the connections between portions of the structure. These can be hydrogen bonds, salt bridges, disulfide bridges and so on (see more at <https://mmcif.wwpdb.org/dictionaries/mmcif_pdbx_v40.dic/Categories/struct_conn.html>).
**wwPDB StructConn extension** in Mol* provides functionality to retrieve and visualize these connections.
The extension exposes three functions, located in `src/extensions/wwpdb/struct-conn/index.ts`.
- `getStructConns` - to retrieve struct_conn records from a loaded structure
- `inspectStructConn` - to visualize a struct_conn
- `clearStructConnInspections` - to remove visulizations created by `inspectStructConn`
## Example 1
The following example is a minimal HTML using this functionality:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="./favicon.ico" type="image/x-icon">
<title>Mol* Viewer</title>
<link rel="stylesheet" type="text/css" href="molstar.css" />
</head>
<body style="margin: 0px;">
<div style="position: absolute; width: 100%; height: 10%; padding-block: 10px;">
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'disulf1');">disulf1</button>
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'disulf2');">disulf2</button>
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'covale1');">covale1</button>
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'covale2');">covale2</button>
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'covale3');">covale3</button>
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'covale4');">covale4</button>
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'metalc1');">metalc1</button>
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'metalc2');">metalc2</button>
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'metalc3');">metalc3</button>
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'metalc4');">metalc4</button>
<button onclick="molstar.PluginExtensions.wwPDBStructConn.clearStructConnInspections(molstarViewer.plugin, '5elb');">CLEAR</button>
</div>
<div id="app" style="position: absolute; top: 10%; width: 100%; height: 90%;"></div>
<script type="text/javascript" src="./molstar.js"></script>
<script type="text/javascript">
var molstarViewer;
molstar.Viewer.create('app', { layoutIsExpanded: false }).then(viewer => {
molstarViewer = viewer;
viewer.loadPdb('5elb');
});
</script>
</body>
</html>
```
The PDB ID (`'5elb'`) can be replaced be `undefined`, in which case the functions will apply to the first loaded structure.
## Example 2
This is a more elaborated example, which automatically loads `5elb` (or any PDB entry given in the URL after `?pdb=`), retrieves the list of struct_conns, and creates a button for each struct_conn.
Be aware that some of the struct_conns may be present in the deposited model but not in the preferred assembly (default view). The presented example will raise a dialog window with error message in such cases, e.g. `disulf6` in entry `5elb`.
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="./favicon.ico" type="image/x-icon">
<title>Mol* Viewer - StructConn Extension Demo</title>
<link rel="stylesheet" type="text/css" href="molstar.css" />
</head>
<style>
body { margin: 0px; }
#app { position: absolute; width: 85%; height: 100%; }
#controls { position: absolute; right: 0; width: 15%; height: 100%; display: flex; flex-direction: column; overflow-y: scroll; }
h1 { text-align: center; margin: 12px; font-weight: bold; font-size: 120%; }
button { margin: 4px; margin-top: 0px; }
</style>
<body>
<div id="app"></div>
<div id="controls">
<h1 id="pdb-id">Loading...</h1>
<button onclick="clearInspections();">CLEAR</button>
</div>
<script type="text/javascript" src="./molstar.js"></script>
<script type="text/javascript">
var pdbId = window.location.search.match(/[?&]pdb=(\w+)/i)?.[1]?.toLowerCase() ?? '5elb';
var molstarViewer;
function inspect(structConnId) {
if (molstarViewer?.plugin) {
molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, pdbId, structConnId).then(nSelectedAtoms => {
if (nSelectedAtoms < 2) alert('Some of the interacting atoms were not found :(\n(maybe not present in the viewed assembly)');
});
}
}
function clearInspections() {
if (molstarViewer?.plugin) {
molstar.PluginExtensions.wwPDBStructConn.clearStructConnInspections(molstarViewer.plugin, pdbId);
}
}
molstar.Viewer.create('app', { layoutIsExpanded: false }).then(viewer => {
molstarViewer = viewer;
return viewer.loadPdb(pdbId);
}).then(() => {
const structConns = molstar.PluginExtensions.wwPDBStructConn.getStructConns(molstarViewer.plugin, pdbId);
const controls = document.getElementById('controls');
for (const structConnId in structConns) {
const button = document.createElement('button');
button.innerText = structConnId;
button.addEventListener('click', () => inspect(structConnId));
controls.appendChild(button);
};
document.getElementById('pdb-id').innerHTML = pdbId;
});
</script>
</body>
</html>
```
......@@ -49,6 +49,7 @@ import { ObjectKeys } from '../../mol-util/type-helpers';
import { SaccharideCompIdMapType } from '../../mol-model/structure/structure/carbohydrates/constants';
import { Backgrounds } from '../../extensions/backgrounds';
import { SbNcbrPartialCharges, SbNcbrPartialChargesPreset, SbNcbrPartialChargesPropertyProvider } from '../../extensions/sb-ncbr';
import { wwPDBStructConnExtensionFunctions } from '../../extensions/wwpdb/struct-conn';
export { PLUGIN_VERSION as version } from '../../mol-plugin/version';
export { setDebugMode, setProductionMode, setTimingMode, consoleStats } from '../../mol-util/debug';
......@@ -512,3 +513,7 @@ export const ViewerAutoPreset = StructureRepresentationPresetProvider({
}
}
});
export const PluginExtensions = {
wwPDBStructConn: wwPDBStructConnExtensionFunctions,
};
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { Column } from '../../../mol-data/db';
import { MmcifFormat } from '../../../mol-model-formats/structure/mmcif';
import { Model } from '../../../mol-model/structure';
import { PluginStateObject } from '../../../mol-plugin-state/objects';
import { StructureComponent } from '../../../mol-plugin-state/transforms/model';
import { StructureRepresentation3D } from '../../../mol-plugin-state/transforms/representation';
import { setSubtreeVisibility } from '../../../mol-plugin/behavior/static/state';
import { PluginContext } from '../../../mol-plugin/context';
import { MolScriptBuilder } from '../../../mol-script/language/builder';
import { ColorNames } from '../../../mol-util/color/names';
/** Amount by which to expand the camera radius when zooming to atoms involved in struct_conn (angstroms) */
const EXTRA_RADIUS = 4;
/** Tags for state tree nodes managed by this extension */
const TAGS = {
RESIDUE_SEL: 'structconn-focus-residue-sel',
ATOM_SEL: 'structconn-focus-atom-sel',
RESIDUE_REPR: 'structconn-focus-residue-repr',
RESIDUE_NCI_REPR: 'structconn-focus-residue-nci-repr',
ATOM_REPR: 'structconn-focus-atom-repr',
} as const;
type VisualParams = ReturnType<typeof StructureRepresentation3D.createDefaultParams>
/** Parameters for 3D representation of atoms involved in struct_conn (pink bubbles) */
const ATOMS_VISUAL_PARAMS: VisualParams = {
type: { name: 'ball-and-stick', params: { sizeFactor: 0.25, sizeAspectRatio: 0.73, adjustCylinderLength: true, xrayShaded: true, aromaticBonds: false, multipleBonds: 'off', dashCount: 1, dashCap: false } },
colorTheme: { name: 'uniform', params: { value: ColorNames.magenta } },
sizeTheme: { name: 'physical', params: {} },
} as const;
/** Parameters for 3D representation of residues involved in struct_conn (normal ball-and-stick) */
const RESIDUES_VISUAL_PARAMS: VisualParams = {
type: { name: 'ball-and-stick', params: { sizeFactor: 0.16 } },
colorTheme: { name: 'element-symbol', params: {} },
sizeTheme: { name: 'physical', params: {} },
} as const;
/** All public functions provided by the StructConn extension */
export const wwPDBStructConnExtensionFunctions = {
/** Return an object with all struct_conn records for a loaded structure.
* Applies to the first structure belonging to `entry` (e.g. '1tqn'),
* or to the first loaded structure overall if `entry` is `undefined`.
*/
getStructConns(plugin: PluginContext, entry: string | undefined): { [id: string]: StructConnRecord } {
const structNode = selectStructureNode(plugin, entry);
const structure = structNode?.obj?.data;
if (structure) return extractStructConns(structure.model);
else return {};
},
/** Create visuals for residues and atoms involved in a struct_conn with ID `structConnId`
* and zoom on them. If `keepExisting` is false (default), remove any such visuals created by previous calls to this function.
* Also hide all carbohydrate SNFG visuals within the structure (as they would occlude our residues of interest).
* Return a promise that resolves to the number of involved atoms which were successfully selected (2, 1, or 0).
*/
async inspectStructConn(plugin: PluginContext, entry: string | undefined, structConnId: string, keepExisting: boolean = false): Promise<number> {
const structNode = selectStructureNode(plugin, entry);
const structure = structNode?.obj?.data;
if (!structure) {
console.error('Structure not found');
return 0;
}
const conns: { [id: string]: StructConnRecord } = structure.model._staticPropertyData['wwpdb-struct-conn-extension-data'] ??= extractStructConns(structure.model);
const conn = conns[structConnId];
if (!conn) {
console.error(`The structure does not contain struct_conn "${structConnId}"`);
return 0;
}
if (!keepExisting) {
await removeAllStructConnInspections(plugin, structNode);
}
const nSelectedAtoms = await addStructConnInspection(plugin, structNode, conn);
hideSnfgNodes(plugin, structNode);
return nSelectedAtoms;
},
/** Remove anything created by `inspectStructConn` within the structure and
* make visible any carbohydrate SNFG visuals that have been hidden by `inspectStructConn`.
*/
async clearStructConnInspections(plugin: PluginContext, entry: string | undefined) {
const structNode = selectStructureNode(plugin, entry);
if (!structNode) return;
await removeAllStructConnInspections(plugin, structNode);
unhideSnfgNodes(plugin, structNode);
},
};
type StructNode = Exclude<ReturnType<typeof selectStructureNode>, undefined>
/** Return the first structure node belonging to `entry` (e.g. '1tqn'),
* or to the first loaded structure node overall if `entry` is `undefined`.
* Includes only "root" structures, not structure components. */
function selectStructureNode(plugin: PluginContext, entry: string | undefined) {
const structNodes = plugin.state.data
.selectQ(q => q.rootsOfType(PluginStateObject.Molecule.Structure));
if (entry) {
const result = structNodes.find(node => node.obj && node.obj.data.model.entry.toLowerCase() === entry.toLowerCase());
if (!result) {
console.warn(`Structure with entry ID "${entry}" was not found. Available structures: ${structNodes.map(node => node.obj?.data.model.entry)}`);
}
return result;
} else {
if (structNodes.length > 1) {
console.warn(`Structure entry ID was not specified, but there is more than one loaded structure (${structNodes.map(node => node.obj?.data.model.entry)}). Taking the first structure.`);
}
if (structNodes.length === 0) {
console.warn(`There are no loaded structures.`);
}
return structNodes[0];
}
}
/** Represents one partner (i.e. atom) of a struct_conn */
interface StructConnPartner {
asymId: string,
seqId: number | undefined,
authSeqId: number | undefined,
insCode: string,
compId: string,
atomId: string,
/** Alternative location (use empty string if not given) */
altId: string,
}
/** Represents a struct_conn (interaction between two partners) */
export interface StructConnRecord {
id: string,
distance: number,
partner1: StructConnPartner,
partner2: StructConnPartner,
}
/** Return an object with all struct_conn records read from mmCIF.
* Return {} if the model comes from another format than mmCIF.
*/
function extractStructConns(model: Model): { [id: string]: StructConnRecord } {
if (!MmcifFormat.is(model.sourceData)) {
console.error('Cannot get struct_conn because source data are not mmCIF.');
return {};
}
const mmcifData = model.sourceData.data;
const {
id,
ptnr1_label_asym_id: asym1,
ptnr1_label_seq_id: seq1,
ptnr1_auth_seq_id: authSeq1,
pdbx_ptnr1_PDB_ins_code: authInsCode1,
ptnr1_label_comp_id: comp1,
ptnr1_label_atom_id: atom1,
pdbx_ptnr1_label_alt_id: alt1,
ptnr2_label_asym_id: asym2,
ptnr2_label_seq_id: seq2,
ptnr2_auth_seq_id: authSeq2,
pdbx_ptnr2_PDB_ins_code: authInsCode2,
ptnr2_label_comp_id: comp2,
ptnr2_label_atom_id: atom2,
pdbx_ptnr2_label_alt_id: alt2,
pdbx_dist_value: distance } = mmcifData.db.struct_conn;
const n = id.rowCount;
const result: { [id: string]: StructConnRecord } = {};
for (let i = 0; i < n; i++) {
const conn: StructConnRecord = {
id: id.value(i),
distance: distance.value(i),
partner1: {
asymId: asym1.value(i),
seqId: seq1.valueKind(i) === Column.ValueKinds.Present ? seq1.value(i) : undefined,
authSeqId: authSeq1.valueKind(i) === Column.ValueKinds.Present ? authSeq1.value(i) : undefined,
insCode: authInsCode1.value(i),
compId: comp1.value(i),
atomId: atom1.value(i),
altId: alt1.value(i),
},
partner2: {
asymId: asym2.value(i),
seqId: seq2.valueKind(i) === Column.ValueKinds.Present ? seq2.value(i) : undefined,
authSeqId: authSeq2.valueKind(i) === Column.ValueKinds.Present ? authSeq2.value(i) : undefined,
insCode: authInsCode2.value(i),
compId: comp2.value(i),
atomId: atom2.value(i),
altId: alt2.value(i),
},
};
result[conn.id] = conn;
}
return result;
}
/** Return MolScript expression for atoms or residues involved in a struct_conn */
function structConnExpression(conn: StructConnRecord, by: 'atoms' | 'residues') {
const { core, struct } = MolScriptBuilder;
const partnerExpressions = [];
for (const partner of [conn.partner1, conn.partner2]) {
const propTests: Parameters<typeof struct.generator.atomGroups>[0] = {
'chain-test': core.rel.eq([struct.atomProperty.macromolecular.label_asym_id(), partner.asymId]),
'group-by': struct.atomProperty.core.operatorName(),
};
if (partner.seqId !== undefined) {
propTests['residue-test'] = core.rel.eq([struct.atomProperty.macromolecular.label_seq_id(), partner.seqId]);
} else if (partner.authSeqId !== undefined) { // for the case of water and carbohydrates (see 5elb, covale3 vs covale5)
propTests['residue-test'] = core.logic.and([
core.rel.eq([struct.atomProperty.macromolecular.auth_seq_id(), partner.authSeqId]),
core.rel.eq([struct.atomProperty.macromolecular.pdbx_PDB_ins_code(), partner.insCode]),
]);
}
if (by === 'residues' && partner.altId !== '') {
propTests['atom-test'] = core.rel.eq([struct.atomProperty.macromolecular.label_alt_id(), partner.altId]);
}
if (by === 'atoms') {
propTests['atom-test'] = core.logic.and([
core.rel.eq([struct.atomProperty.macromolecular.label_atom_id(), partner.atomId]),
core.rel.eq([struct.atomProperty.macromolecular.label_alt_id(), partner.altId]),
]);
}
partnerExpressions.push(struct.filter.first([struct.generator.atomGroups(propTests)]));
}
return struct.combinator.merge(partnerExpressions.map(e => struct.modifier.union([e])));
}
/** Create visuals for residues and atoms involved in a struct_conn and zoom on them.
* Return a promise that resolves to the number of involved atoms which were successfully selected (2, 1, or 0).
*/
async function addStructConnInspection(plugin: PluginContext, structNode: StructNode, conn: StructConnRecord): Promise<number> {
const expressionByResidues = structConnExpression(conn, 'residues');
const expressionByAtoms = structConnExpression(conn, 'atoms');
const update = plugin.build();
update.to(structNode).apply(
StructureComponent,
{ label: `${conn.id} (residues)`, type: { name: 'expression', params: expressionByResidues } },
{ tags: [TAGS.RESIDUE_SEL] }
).apply(
StructureRepresentation3D,
RESIDUES_VISUAL_PARAMS,
{ tags: [TAGS.RESIDUE_REPR] }
);
const atomsSelection = update.to(structNode).apply(
StructureComponent,
{ label: `${conn.id} (atoms)`, type: { name: 'expression', params: expressionByAtoms } },
{ tags: [TAGS.ATOM_SEL] }
);
const atomsVisual = update.to(atomsSelection.ref).apply(
StructureRepresentation3D,
ATOMS_VISUAL_PARAMS,
{ tags: [TAGS.ATOM_REPR] }
);
await update.commit();
plugin.managers.camera.focusRenderObjects(atomsVisual.selector.data?.repr.renderObjects, { extraRadius: EXTRA_RADIUS });
const nSelectedAtoms = atomsSelection.selector.obj?.data?.elementCount ?? 0;
return nSelectedAtoms;
}
/** Remove anything created by `addStructConnInspection` */
async function removeAllStructConnInspections(plugin: PluginContext, structNode: StructNode) {
const selNodes = [
...plugin.state.data.selectQ(q => q.byRef(structNode.transform.ref).subtree().withTag(TAGS.RESIDUE_SEL)),
...plugin.state.data.selectQ(q => q.byRef(structNode.transform.ref).subtree().withTag(TAGS.ATOM_SEL)),
];
const update = plugin.build();
for (const node of selNodes) {
update.delete(node);
}
await update.commit();
}
/** Hide all carbohydrate SNFG visuals */
function hideSnfgNodes(plugin: PluginContext, structNode: StructNode) {
const snfgNodes = plugin.state.data.selectQ(q => q.byRef(structNode.transform.ref).subtree().withTag('branched-snfg-3d'));
for (const node of snfgNodes) {
setSubtreeVisibility(plugin.state.data, node.transform.ref, true); // true means hidden
}
}
/** Make visible all carbohydrate SNFG visuals that have been hidden by `hideSnfgNodes` */
function unhideSnfgNodes(plugin: PluginContext, structNode: StructNode) {
const snfgNodes = plugin.state.data.selectQ(q => q.byRef(structNode.transform.ref).subtree().withTag('branched-snfg-3d'));
for (const node of snfgNodes) {
try {
setSubtreeVisibility(plugin.state.data, node.transform.ref, false); // false means visible
} catch {
// this is OK, the node has been removed
}
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment