mirror of
https://github.com/molstar/molstar.git
synced 2026-06-04 13:30:24 +08:00
Basic support for Predicted Aligned Error parsing and plotting (#1302)
* proof of concept * typos * plot interactivity * centerline * better axis labels, darkmode support * plot label * data source selection * interactivity * better plot labels * make pae overpaint ghosts * config option * update labels * standalone mode and AFDB example * fix interactivity * pr feedback * changelog
This commit is contained in:
@@ -20,6 +20,10 @@ Note that since we don't clearly distinguish between a public and private interf
|
||||
- MolViewSpec: MVP Support for geometrical primitives (mesh, lines, line, label, distance measurement)
|
||||
- Mesoscale Explorer: Add support for 4-character PDB IDs (e.g., 8ZZC) in PDB-Dev loader
|
||||
- Fix Sequence View in Safari 18
|
||||
- ModelArchive QualityAssessment extension:
|
||||
- Add support for ma_qa_metric_local_pairwise mmCIF category
|
||||
- Add PAE plot component
|
||||
- Added new AlphaFoldDB-PAE example
|
||||
- Add support for LAMMPS data and dump formats
|
||||
|
||||
## [v4.7.1] - 2024-09-30
|
||||
|
||||
@@ -876,6 +876,17 @@ ma_qa_metric_local.metric_value
|
||||
ma_qa_metric_local.model_id
|
||||
ma_qa_metric_local.ordinal_id
|
||||
|
||||
ma_qa_metric_local_pairwise.ordinal_id
|
||||
ma_qa_metric_local_pairwise.model_id
|
||||
ma_qa_metric_local_pairwise.label_asym_id_1
|
||||
ma_qa_metric_local_pairwise.label_comp_id_1
|
||||
ma_qa_metric_local_pairwise.label_seq_id_1
|
||||
ma_qa_metric_local_pairwise.label_asym_id_2
|
||||
ma_qa_metric_local_pairwise.label_comp_id_2
|
||||
ma_qa_metric_local_pairwise.label_seq_id_2
|
||||
ma_qa_metric_local_pairwise.metric_id
|
||||
ma_qa_metric_local_pairwise.metric_value
|
||||
|
||||
ma_software_group.group_id
|
||||
ma_software_group.ordinal_id
|
||||
ma_software_group.software_id
|
||||
|
||||
|
@@ -12,7 +12,7 @@ import { Backgrounds } from '../../extensions/backgrounds';
|
||||
import { DnatcoNtCs } from '../../extensions/dnatco';
|
||||
import { G3DFormat, G3dProvider } from '../../extensions/g3d/format';
|
||||
import { GeometryExport } from '../../extensions/geo-export';
|
||||
import { MAQualityAssessment, QualityAssessmentPLDDTPreset, QualityAssessmentQmeanPreset } from '../../extensions/model-archive/quality-assessment/behavior';
|
||||
import { MAQualityAssessment, MAQualityAssessmentConfig, QualityAssessmentPLDDTPreset, QualityAssessmentQmeanPreset } from '../../extensions/model-archive/quality-assessment/behavior';
|
||||
import { QualityAssessment } from '../../extensions/model-archive/quality-assessment/prop';
|
||||
import { ModelExport } from '../../extensions/model-export';
|
||||
import { Mp4Export } from '../../extensions/mp4-export';
|
||||
@@ -48,7 +48,7 @@ import { createPluginUI } from '../../mol-plugin-ui';
|
||||
import { renderReact18 } from '../../mol-plugin-ui/react18';
|
||||
import { DefaultPluginUISpec, PluginUISpec } from '../../mol-plugin-ui/spec';
|
||||
import { PluginCommands } from '../../mol-plugin/commands';
|
||||
import { PluginConfig } from '../../mol-plugin/config';
|
||||
import { PluginConfig, PluginConfigItem } from '../../mol-plugin/config';
|
||||
import { PluginLayoutControlsDisplay } from '../../mol-plugin/layout';
|
||||
import { PluginSpec } from '../../mol-plugin/spec';
|
||||
import { PluginState } from '../../mol-plugin/state';
|
||||
@@ -125,6 +125,8 @@ const DefaultViewerOptions = {
|
||||
rcsbAssemblySymmetryDefaultServerType: AssemblySymmetryConfig.DefaultServerType.defaultValue,
|
||||
rcsbAssemblySymmetryDefaultServerUrl: AssemblySymmetryConfig.DefaultServerUrl.defaultValue,
|
||||
rcsbAssemblySymmetryApplyColors: AssemblySymmetryConfig.ApplyColors.defaultValue,
|
||||
|
||||
config: [] as [PluginConfigItem, any][],
|
||||
};
|
||||
type ViewerOptions = typeof DefaultViewerOptions;
|
||||
|
||||
@@ -204,6 +206,7 @@ export class Viewer {
|
||||
[AssemblySymmetryConfig.DefaultServerType, o.rcsbAssemblySymmetryDefaultServerType],
|
||||
[AssemblySymmetryConfig.DefaultServerUrl, o.rcsbAssemblySymmetryDefaultServerUrl],
|
||||
[AssemblySymmetryConfig.ApplyColors, o.rcsbAssemblySymmetryApplyColors],
|
||||
...(o.config ?? []),
|
||||
]
|
||||
};
|
||||
|
||||
@@ -616,4 +619,9 @@ export const ViewerAutoPreset = StructureRepresentationPresetProvider({
|
||||
export const PluginExtensions = {
|
||||
wwPDBStructConn: wwPDBStructConnExtensionFunctions,
|
||||
mvs: { MVSData, loadMVS },
|
||||
modelArchive: {
|
||||
qualityAssessment: {
|
||||
config: MAQualityAssessmentConfig
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
58
src/examples/alphafolddb-pae/index.html
Normal file
58
src/examples/alphafolddb-pae/index.html
Normal file
@@ -0,0 +1,58 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
|
||||
<title>Mol* AlphaFold DB Predicted Aligned Error Example</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
#app {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
width: 640px;
|
||||
height: 480px;
|
||||
}
|
||||
|
||||
#plot {
|
||||
position: absolute;
|
||||
left: 680px;
|
||||
top: 20px;
|
||||
width: 480px;
|
||||
height: 480px;
|
||||
}
|
||||
|
||||
#controls {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
top: 520px;
|
||||
font-family: sans-serif;
|
||||
font-size: smaller;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" type="text/css" href="molstar.css" />
|
||||
<script type="text/javascript" src="./index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id='controls'>
|
||||
<input type='text' id='af-id' value='Q8W3K0' />
|
||||
<button id='af-load'>Load</button>
|
||||
</div>
|
||||
<div id='app'></div>
|
||||
<div id='plot'></div>
|
||||
<script>
|
||||
AlphaFoldPAEExample.init({ pluginContainerId: 'app', plotContainerId: 'plot' }).then(example => {
|
||||
example.load('Q8W3K0')
|
||||
});
|
||||
|
||||
function $(id) { return document.getElementById(id); }
|
||||
$('af-load').onclick = () => AlphaFoldPAEExample.load($('af-id').value)
|
||||
</script>
|
||||
<!-- __MOLSTAR_ANALYTICS__ -->
|
||||
</body>
|
||||
</html>
|
||||
96
src/examples/alphafolddb-pae/index.tsx
Normal file
96
src/examples/alphafolddb-pae/index.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { Viewer } from '../../apps/viewer/app';
|
||||
import { MAPairwiseScorePlot } from '../../extensions/model-archive/quality-assessment/pairwise/ui';
|
||||
import { QualityAssessment } from '../../extensions/model-archive/quality-assessment/prop';
|
||||
import { Model, ResidueIndex } from '../../mol-model/structure';
|
||||
import './index.html';
|
||||
require('mol-plugin-ui/skin/light.scss');
|
||||
|
||||
export class AlphaFoldPAEExample {
|
||||
viewer: Viewer;
|
||||
plotContainerId: string;
|
||||
|
||||
|
||||
async init(options: { pluginContainerId: string, plotContainerId: string }) {
|
||||
this.plotContainerId = options.plotContainerId;
|
||||
this.viewer = await Viewer.create(options.pluginContainerId, {
|
||||
layoutIsExpanded: false,
|
||||
layoutShowControls: false,
|
||||
layoutShowLeftPanel: false,
|
||||
layoutShowLog: false,
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
async load(afId: string) {
|
||||
const id = afId.trim().toUpperCase();
|
||||
|
||||
const plotRoot = createRoot(document.getElementById(this.plotContainerId)!);
|
||||
plotRoot.render(<div>Loading...</div>);
|
||||
|
||||
await this.viewer.plugin.clear();
|
||||
await this.viewer.loadAlphaFoldDb(id);
|
||||
|
||||
try {
|
||||
const req = await fetch(`https://alphafold.ebi.ac.uk/files/AF-${id}-F1-predicted_aligned_error_v4.json`);
|
||||
const json = await req.json();
|
||||
|
||||
const model = this.viewer.plugin.managers.structure.hierarchy.current.models[0]?.cell.obj?.data!;
|
||||
const metric = pairwiseMetricFromAlphaFoldDbJson(model, json)!;
|
||||
|
||||
createRoot(document.getElementById(this.plotContainerId)!).render(
|
||||
<div className='msp-plugin' style={{ background: 'white' }}>
|
||||
<MAPairwiseScorePlot plugin={this.viewer.plugin} pairwiseMetric={metric} model={model} />
|
||||
</div>
|
||||
);
|
||||
} catch (err) {
|
||||
plotRoot.render(<div>Error: {String(err)}</div>);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function pairwiseMetricFromAlphaFoldDbJson(model: Model, data: any): QualityAssessment.Pairwise | undefined {
|
||||
if (!Array.isArray(data) || !data[0]?.predicted_aligned_error) return undefined;
|
||||
|
||||
const { residues, residueAtomSegments, atomSourceIndex } = model.atomicHierarchy;
|
||||
const sortedResidueIndices = new Array(residues._rowCount).fill(0).map((_, i) => i);
|
||||
sortedResidueIndices.sort((a, b) => {
|
||||
const idxA = atomSourceIndex.value(residueAtomSegments.offsets[a]);
|
||||
const idxB = atomSourceIndex.value(residueAtomSegments.offsets[b]);
|
||||
return idxA - idxB;
|
||||
});
|
||||
|
||||
const metricData = data[0].predicted_aligned_error as number[][];
|
||||
|
||||
const metric: QualityAssessment.Pairwise = {
|
||||
id: 0,
|
||||
name: 'AlphaFold DB PAE',
|
||||
residueRange: [0 as ResidueIndex, (residues._rowCount - 1) as ResidueIndex],
|
||||
valueRange: [0, data[0].max_predicted_aligned_error],
|
||||
values: {}
|
||||
};
|
||||
|
||||
for (let i = 0; i < metricData.length; i++) {
|
||||
const rA = sortedResidueIndices[i];
|
||||
if (typeof rA !== 'number') continue;
|
||||
const row = metricData[i];
|
||||
const xs: any = (metric.values[rA as ResidueIndex] = {});
|
||||
for (let j = 0; j < row.length; j++) {
|
||||
const rB = sortedResidueIndices[j];
|
||||
if (typeof rB !== 'number') continue;
|
||||
xs[rB] = row[j];
|
||||
}
|
||||
}
|
||||
|
||||
return metric;
|
||||
}
|
||||
|
||||
(window as any).AlphaFoldPAEExample = new AlphaFoldPAEExample();
|
||||
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2021-24 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
@@ -17,6 +18,12 @@ import { cantorPairing } from '../../../mol-data/util';
|
||||
import { QmeanScoreColorThemeProvider } from './color/qmean';
|
||||
import { PresetStructureRepresentations, StructureRepresentationPresetProvider } from '../../../mol-plugin-state/builder/structure/representation-preset';
|
||||
import { StateObjectRef } from '../../../mol-state';
|
||||
import { MAPairwiseScorePlotPanel } from './pairwise/ui';
|
||||
import { PluginConfigItem } from '../../../mol-plugin/config';
|
||||
|
||||
export const MAQualityAssessmentConfig = {
|
||||
EnablePairwiseScorePlot: new PluginConfigItem('ma-quality-assessment-prop.enable-pairwise-score-plot', true),
|
||||
};
|
||||
|
||||
export const MAQualityAssessment = PluginBehavior.create<{ autoAttach: boolean, showTooltip: boolean }>({
|
||||
name: 'ma-quality-assessment-prop',
|
||||
@@ -52,6 +59,10 @@ export const MAQualityAssessment = PluginBehavior.create<{ autoAttach: boolean,
|
||||
|
||||
this.ctx.builders.structure.representation.registerPreset(QualityAssessmentPLDDTPreset);
|
||||
this.ctx.builders.structure.representation.registerPreset(QualityAssessmentQmeanPreset);
|
||||
|
||||
if (this.ctx.config.get(MAQualityAssessmentConfig.EnablePairwiseScorePlot)) {
|
||||
this.ctx.customStructureControls.set('ma-quality-assessment-pairwise-plot', MAPairwiseScorePlotPanel as any);
|
||||
}
|
||||
}
|
||||
|
||||
update(p: { autoAttach: boolean, showTooltip: boolean }) {
|
||||
@@ -76,6 +87,8 @@ export const MAQualityAssessment = PluginBehavior.create<{ autoAttach: boolean,
|
||||
|
||||
this.ctx.builders.structure.representation.unregisterPreset(QualityAssessmentPLDDTPreset);
|
||||
this.ctx.builders.structure.representation.unregisterPreset(QualityAssessmentQmeanPreset);
|
||||
|
||||
this.ctx.customStructureControls.delete('ma-quality-assessment-pairwise-plot');
|
||||
}
|
||||
},
|
||||
params: () => ({
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { Model, ResidueIndex } from '../../../../mol-model/structure';
|
||||
import { AtomicHierarchy } from '../../../../mol-model/structure/model/properties/atomic';
|
||||
import { Color } from '../../../../mol-util/color';
|
||||
import { QualityAssessment } from '../prop';
|
||||
|
||||
|
||||
const DefaultMetricColorRange = [0x00441B, 0xF7FCF5] as [Color, Color];
|
||||
|
||||
export type MAResidueRangeInfo = { startOffset: number, endOffset: number, label: string };
|
||||
|
||||
function drawMetricPNG(model: Model, metric: QualityAssessment.Pairwise, colorRange: [Color, Color], noDataColor: Color) {
|
||||
const [minResidueIndex, maxResidueIndex] = metric.residueRange;
|
||||
const [minMetric, maxMetric] = metric.valueRange;
|
||||
const [minColor, maxColor] = colorRange;
|
||||
const range = maxResidueIndex - minResidueIndex + 1;
|
||||
const valueRange = maxMetric - minMetric;
|
||||
const values = metric.values;
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = range;
|
||||
canvas.height = range;
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
ctx.fillStyle = Color.toStyle(noDataColor);
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
for (let rA = minResidueIndex; rA <= maxResidueIndex; rA++) {
|
||||
const row = values[rA];
|
||||
if (!row) continue;
|
||||
|
||||
for (let rB = minResidueIndex; rB <= maxResidueIndex; rB++) {
|
||||
const value = row[rB];
|
||||
if (typeof value !== 'number') continue;
|
||||
|
||||
const x = rA - minResidueIndex;
|
||||
const y = rB - minResidueIndex;
|
||||
const t = (value - minMetric) / valueRange;
|
||||
|
||||
const color = Color.interpolate(minColor, maxColor, t);
|
||||
ctx.fillStyle = Color.toStyle(color);
|
||||
ctx.fillRect(x, y, 1, 1);
|
||||
ctx.fillRect(y, x, 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
const chains: MAResidueRangeInfo[] = [];
|
||||
const hierarchy = model.atomicHierarchy;
|
||||
const { label_asym_id } = hierarchy.chains;
|
||||
|
||||
let cI = AtomicHierarchy.residueChainIndex(hierarchy, minResidueIndex as ResidueIndex);
|
||||
let currentChain: MAResidueRangeInfo = { startOffset: 0, endOffset: 1, label: label_asym_id.value(cI) };
|
||||
chains.push(currentChain);
|
||||
|
||||
for (let i = 1; i < range; i++) {
|
||||
cI = AtomicHierarchy.residueChainIndex(hierarchy, (minResidueIndex + i) as ResidueIndex);
|
||||
const asym_id = label_asym_id.value(cI);
|
||||
if (asym_id === currentChain.label) {
|
||||
currentChain.endOffset = i + 1;
|
||||
} else {
|
||||
currentChain = { startOffset: i, endOffset: i + 1, label: asym_id };
|
||||
chains.push(currentChain);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
model,
|
||||
metric,
|
||||
chains,
|
||||
colorRange: [Color.toStyle(colorRange[0]), Color.toStyle(colorRange[1])] as const,
|
||||
png: canvas.toDataURL('png')
|
||||
};
|
||||
}
|
||||
|
||||
export function maDrawPairwiseMetricPNG(model: Model, metric: QualityAssessment.Pairwise) {
|
||||
return drawMetricPNG(model, metric, DefaultMetricColorRange, Color(0xE2E2E2));
|
||||
}
|
||||
|
||||
export type MAPairwiseMetricDrawing = ReturnType<typeof drawMetricPNG>
|
||||
504
src/extensions/model-archive/quality-assessment/pairwise/ui.tsx
Normal file
504
src/extensions/model-archive/quality-assessment/pairwise/ui.tsx
Normal file
@@ -0,0 +1,504 @@
|
||||
/**
|
||||
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { CSSProperties, Fragment, memo, ReactNode, useEffect, useRef } from 'react';
|
||||
import { BehaviorSubject, combineLatest, distinctUntilChanged, throttleTime } from 'rxjs';
|
||||
import { clamp } from '../../../../mol-math/interpolate';
|
||||
import { Model, ResidueIndex, StructureElement, StructureProperties, StructureQuery } from '../../../../mol-model/structure';
|
||||
import { AtomicHierarchy } from '../../../../mol-model/structure/model/properties/atomic';
|
||||
import { atoms } from '../../../../mol-model/structure/query/queries/generators';
|
||||
import { PluginStateObject } from '../../../../mol-plugin-state/objects';
|
||||
import { OverpaintStructureRepresentation3DFromBundle } from '../../../../mol-plugin-state/transforms/representation';
|
||||
import { CollapsableControls, CollapsableState } from '../../../../mol-plugin-ui/base';
|
||||
import { ScatterPlotSvg } from '../../../../mol-plugin-ui/controls/icons';
|
||||
import { ParameterControls } from '../../../../mol-plugin-ui/controls/parameters';
|
||||
import { useBehavior } from '../../../../mol-plugin-ui/hooks/use-behavior';
|
||||
import { PluginContext } from '../../../../mol-plugin/context';
|
||||
import { StateBuilder, StateTransform } from '../../../../mol-state';
|
||||
import { round } from '../../../../mol-util';
|
||||
import { Color } from '../../../../mol-util/color';
|
||||
import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
|
||||
import { SingleAsyncQueue } from '../../../../mol-util/single-async-queue';
|
||||
import { QualityAssessment } from '../prop';
|
||||
import { maDrawPairwiseMetricPNG, MAPairwiseMetricDrawing } from './plot';
|
||||
|
||||
type State = ReturnType<typeof getPropsAndValues>
|
||||
|
||||
export class MAPairwiseScorePlotPanel extends CollapsableControls<{}, State> {
|
||||
protected defaultState(): State & CollapsableState {
|
||||
return {
|
||||
header: 'Predicted Aligned Error',
|
||||
isCollapsed: false,
|
||||
isHidden: true,
|
||||
brand: { accent: 'purple', svg: ScatterPlotSvg },
|
||||
params: {} as any,
|
||||
values: undefined as any,
|
||||
dataSources: [],
|
||||
};
|
||||
}
|
||||
|
||||
toggleCollapsed() {
|
||||
if (!this.state.isCollapsed) {
|
||||
this.setState({ isCollapsed: true });
|
||||
} else {
|
||||
const state = getPropsAndValues(this.plugin, this.state.values);
|
||||
this.setState({
|
||||
...state,
|
||||
isCollapsed: false,
|
||||
isHidden: state.params.data.options.length === 0 || state.params.model.options.length === 0
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
interactivity = new BehaviorSubject<PlotInteractivityState>({});
|
||||
queue = new SingleAsyncQueue();
|
||||
|
||||
componentDidMount() {
|
||||
this.subscribe(combineLatest([
|
||||
this.plugin.state.data.events.changed,
|
||||
this.plugin.behaviors.state.isAnimating
|
||||
]), ([_, anim]) => {
|
||||
if (anim || this.state.isCollapsed) return;
|
||||
const state = getPropsAndValues(this.plugin, this.state.values);
|
||||
this.setState({
|
||||
...state,
|
||||
isHidden: state.params.data.options.length === 0 || state.params.model.options.length === 0
|
||||
});
|
||||
});
|
||||
|
||||
this.subscribe(filterHighlightState(this.interactivity), state => {
|
||||
highlightState(this.plugin, state);
|
||||
});
|
||||
this.subscribe(filterOverpaintState(this.interactivity), state => {
|
||||
this.queue.enqueue(() => overpaintState(this.plugin, state));
|
||||
});
|
||||
}
|
||||
|
||||
protected renderControls(): JSX.Element | null {
|
||||
const { params, values, dataSources } = this.state;
|
||||
return <>
|
||||
<ParameterControls params={params} values={values} onChangeValues={values => this.setState({ values })} />
|
||||
<PlotWrapper plugin={this.plugin} values={values} dataSources={dataSources} interactivity={this.interactivity} />
|
||||
</>;
|
||||
}
|
||||
}
|
||||
|
||||
export function MAPairwiseScorePlot({ plugin, model, pairwiseMetric }: { plugin: PluginContext, model: Model, pairwiseMetric: QualityAssessment.Pairwise }) {
|
||||
const _interactivity = useRef<BehaviorSubject<PlotInteractivityState>>();
|
||||
const interactivity = _interactivity.current ??= new BehaviorSubject<PlotInteractivityState>({});
|
||||
|
||||
useEffect(() => {
|
||||
const queue = new SingleAsyncQueue();
|
||||
|
||||
const highlight = filterHighlightState(interactivity).subscribe(state => highlightState(plugin, state));
|
||||
const paint = filterOverpaintState(interactivity).subscribe(state => queue.enqueue(() => overpaintState(plugin, state)));
|
||||
|
||||
return () => {
|
||||
highlight.unsubscribe();
|
||||
paint.unsubscribe();
|
||||
queue.enqueue(() => overpaintState(plugin, interactivity.value));
|
||||
};
|
||||
}, [model, pairwiseMetric]);
|
||||
|
||||
return <MAPairwiseScorePlotBase model={model} pairwiseMetric={pairwiseMetric} interactivity={interactivity} />;
|
||||
}
|
||||
|
||||
function filterHighlightState(state: BehaviorSubject<PlotInteractivityState>) {
|
||||
return state.pipe(
|
||||
throttleTime(16, undefined, { leading: true, trailing: true }),
|
||||
distinctUntilChanged((a, b) => a.crosshairOffset === b.crosshairOffset)
|
||||
);
|
||||
}
|
||||
|
||||
function filterOverpaintState(state: BehaviorSubject<PlotInteractivityState>) {
|
||||
return state.pipe(
|
||||
throttleTime(66, undefined, { leading: true, trailing: true }),
|
||||
distinctUntilChanged((a, b) => a.boxStart === b.boxStart && (a.mouseDown ? a.crosshairOffset : a.boxEnd) === (b.mouseDown ? b.crosshairOffset : b.boxEnd))
|
||||
);
|
||||
}
|
||||
|
||||
const PlotWrapper = memo(({ plugin, values, dataSources, interactivity }: { plugin: PluginContext, values: State['values'], dataSources: State['dataSources'], interactivity: BehaviorSubject<PlotInteractivityState> }) => {
|
||||
const model: Model | undefined = plugin.managers.structure.hierarchy.current.models.find(m => m.cell.transform.ref === values.model)?.cell.obj?.data;
|
||||
const src = dataSources.find(src => src.id === values.data);
|
||||
const cif: PluginStateObject.Format.Cif | undefined = plugin.state.data.cells.get(src?.dataRef!)?.obj;
|
||||
const block = cif?.data.blocks[src?.blockIndex!];
|
||||
|
||||
if (!model || !block || !src) return <div className='msp-description'>Data not available</div>;
|
||||
|
||||
const metric = QualityAssessment.pairwiseMetricFromModelArchiveCIF(model, block, src.metridId);
|
||||
if (!metric) return <div className='msp-description'>Data not available</div>;
|
||||
|
||||
return <MAPairwiseScorePlotBase interactivity={interactivity} model={model} pairwiseMetric={metric} />;
|
||||
}, (prev, next) => prev.values.data === next.values.data && prev.values.model === next.values.model);
|
||||
|
||||
function getPropsAndValues(plugin: PluginContext, current?: { model?: string, data?: string }) {
|
||||
const models = plugin.managers.structure.hierarchy.current.models;
|
||||
const cifs = plugin.state.data.selectQ(q => q.root.subtree().ofType(PluginStateObject.Format.Cif));
|
||||
|
||||
const dataSources: {
|
||||
id: string,
|
||||
label: string,
|
||||
metridId: number,
|
||||
dataRef: StateTransform.Ref,
|
||||
blockIndex: number,
|
||||
}[] = [];
|
||||
|
||||
for (const cif of cifs) {
|
||||
if (!cif.obj?.data.blocks) continue;
|
||||
let blockIndex = 0;
|
||||
for (const block of cif.obj.data.blocks) {
|
||||
for (const pae of QualityAssessment.findModelArchiveCIFPAEMetrics(block)) {
|
||||
dataSources.push({
|
||||
id: `${cif.transform.ref}:${blockIndex}:${pae.id}`,
|
||||
metridId: pae.id,
|
||||
label: `${block.header}: ${pae.name}`,
|
||||
dataRef: cif.transform.ref,
|
||||
blockIndex,
|
||||
});
|
||||
}
|
||||
blockIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
const params = {
|
||||
model: PD.Select(models[0]?.cell.transform.ref, models.map(m => [m.cell.transform.ref, m.cell.obj?.data.label!]), { isHidden: models.length <= 1 }),
|
||||
data: PD.Select(dataSources[0]?.id, dataSources.map(o => [o.id, o.label]), { isHidden: dataSources.length <= 1 })
|
||||
};
|
||||
|
||||
const values = {
|
||||
model: params.model.options.find(o => o[0] === current?.model)?.[0] ?? params.model.options[0]?.[0],
|
||||
data: params.data.options.find(o => o[0] === current?.data)?.[0] ?? params.data.options[0]?.[0],
|
||||
};
|
||||
|
||||
return { params, values, dataSources };
|
||||
}
|
||||
|
||||
const PlotSize = 1000;
|
||||
const PlotOffset = 120;
|
||||
|
||||
const PlotColors = {
|
||||
ScoredOverpaint: Color(0xFFA500),
|
||||
ScoredLabel: Color(0xBC7100),
|
||||
AlignedOverpaint: Color(0x1AFFBB),
|
||||
AlignedLabel: Color(0x0F8E68),
|
||||
};
|
||||
|
||||
interface PlotInteractivityState {
|
||||
model?: Model;
|
||||
drawing?: MAPairwiseMetricDrawing;
|
||||
crosshairOffset?: [number, number];
|
||||
inside?: boolean;
|
||||
mouseDown?: boolean;
|
||||
boxStart?: [number, number];
|
||||
boxEnd?: [number, number];
|
||||
}
|
||||
|
||||
export const MAPairwiseScorePlotBase = memo(({ model, pairwiseMetric, interactivity }: { model: Model, pairwiseMetric: QualityAssessment.Pairwise, interactivity: BehaviorSubject<PlotInteractivityState> }) => {
|
||||
const interactivityRect = useRef<SVGRectElement>();
|
||||
const drawing = maDrawPairwiseMetricPNG(model, pairwiseMetric);
|
||||
|
||||
useEffect(() => {
|
||||
if (!drawing) {
|
||||
interactivity.next({});
|
||||
return;
|
||||
}
|
||||
interactivity.next({ model, drawing });
|
||||
const moveEvent = (ev: MouseEvent) => {
|
||||
const current = interactivity.value;
|
||||
if (!current.inside && !current.mouseDown) return;
|
||||
|
||||
const offset = getPlotMouseOffsetBase(interactivityRect.current!, ev.clientX, ev.clientY);
|
||||
interactivity.next({ ...current, crosshairOffset: offset });
|
||||
};
|
||||
const mouseUpEvent = (ev: MouseEvent) => {
|
||||
if (!interactivity.value.mouseDown) return;
|
||||
const offset = getPlotMouseOffsetBase(interactivityRect.current!, ev.clientX, ev.clientY);
|
||||
interactivity.next({ ...interactivity.value, mouseDown: false, boxEnd: offset });
|
||||
};
|
||||
window.addEventListener('mousemove', moveEvent);
|
||||
window.addEventListener('mouseup', mouseUpEvent);
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', moveEvent);
|
||||
window.removeEventListener('mouseup', mouseUpEvent);
|
||||
};
|
||||
}, [model, interactivity, drawing]);
|
||||
|
||||
if (!drawing) return <>Not available</>;
|
||||
|
||||
|
||||
const { metric, colorRange, chains, png } = drawing;
|
||||
const nResidues = metric.residueRange[1] - metric.residueRange[0];
|
||||
|
||||
const border = '#333';
|
||||
const line = '#000';
|
||||
|
||||
const legendHeight = 80;
|
||||
const legendOffsetY = PlotOffset + PlotSize + 50;
|
||||
|
||||
const viewBox = '0 0 1140 1270';
|
||||
|
||||
return <div style={{ margin: '8px 8px 0 8px', position: 'relative' }}>
|
||||
<svg viewBox={viewBox} width='100%'>
|
||||
<image x={PlotOffset + 1} y={PlotOffset + 1} width={PlotSize - 1} height={PlotSize - 1} href={png} />
|
||||
<line x1={PlotOffset} x2={PlotOffset + PlotSize} y1={PlotOffset} y2={PlotOffset + PlotSize} style={{ stroke: line, strokeDasharray: '15,15' }} />
|
||||
<linearGradient id='legend-gradient' x1={0} x2={1} y1={0} y2={0}>
|
||||
<stop offset='0%' stopColor={colorRange[0]} />
|
||||
<stop offset='100%' stopColor={colorRange[1]} />
|
||||
</linearGradient>
|
||||
<rect x={PlotOffset} y={legendOffsetY} width={PlotSize} height={legendHeight} style={{ fill: 'url(#legend-gradient)', strokeWidth: 1, stroke: border }} />
|
||||
<text x={PlotOffset + 20} y={legendOffsetY + legendHeight - 22} style={{ fontSize: '45px', fill: 'white', fontWeight: 'bold' }}>{round(metric.valueRange[0], 2)} Å</text>
|
||||
<text x={PlotOffset + PlotSize - 20} y={legendOffsetY + legendHeight - 22} style={{ fontSize: '45px', fill: 'black', fontWeight: 'bold' }} textAnchor='end'>{round(metric.valueRange[1], 2)} Å</text>
|
||||
<text x={PlotOffset + PlotSize / 2} y={legendOffsetY + legendHeight - 22} style={{ fontSize: '45px', fill: 'black' }} textAnchor='middle'>Predicted Aligned Error</text>
|
||||
|
||||
<text x={PlotOffset + PlotSize / 2} y={50} style={{ fontSize: '45px', fontWeight: 'bold', fill: Color.toStyle(PlotColors.ScoredLabel) }} textAnchor='middle'>Scored Residue</text>
|
||||
<text className='msp-svg-text' style={{ fontSize: '50px', fontWeight: 'bold', fill: Color.toStyle(PlotColors.AlignedLabel) }} transform={`translate(50, ${PlotOffset + PlotSize / 2}) rotate(270)`} textAnchor='middle'>Aligned Residue</text>
|
||||
|
||||
{chains.map(({ startOffset, endOffset, label }) => {
|
||||
const textOffset = PlotOffset + PlotSize * (startOffset + (endOffset - startOffset) / 2) / nResidues;
|
||||
const endLineOffset = PlotOffset + PlotSize * endOffset / nResidues;
|
||||
const startLineOffset = PlotOffset + PlotSize * startOffset / nResidues;
|
||||
|
||||
const seq_id = model.atomicHierarchy.residues.label_seq_id;
|
||||
const startIndex = seq_id.value(metric.residueRange[0] + startOffset);
|
||||
const endIndex = seq_id.value(metric.residueRange[0] + endOffset - 1);
|
||||
|
||||
return <Fragment key={startOffset}>
|
||||
<text x={textOffset} y={PlotOffset - 15} className='msp-svg-text' style={{ fontSize: '40px' }} textAnchor='middle'>{label} {startIndex}-{endIndex}</text>
|
||||
<text className='msp-svg-text' style={{ fontSize: '40px' }} transform={`translate(${PlotOffset - 15}, ${textOffset}) rotate(270)`} textAnchor='middle'>{label} {startIndex}-{endIndex}</text>
|
||||
<line x1={startLineOffset} x2={startLineOffset} y1={PlotOffset - 20} y2={PlotOffset + PlotSize + 20} style={{ stroke: line, strokeDasharray: '15,15' }} />
|
||||
<line x1={endLineOffset} x2={endLineOffset} y1={PlotOffset - 20} y2={PlotOffset + PlotSize + 20} style={{ stroke: line, strokeDasharray: '15,15' }} />
|
||||
<line x1={PlotOffset - 20} x2={PlotOffset + PlotSize + 20} y1={startLineOffset} y2={startLineOffset} style={{ stroke: line, strokeDasharray: '15,15' }} />
|
||||
<line x1={PlotOffset - 20} x2={PlotOffset + PlotSize + 20} y1={endLineOffset} y2={endLineOffset} style={{ stroke: line, strokeDasharray: '15,15' }} />
|
||||
</Fragment>;
|
||||
})}
|
||||
</svg>
|
||||
<svg viewBox={viewBox} style={{ position: 'absolute', inset: 0 }}>
|
||||
<rect x={PlotOffset} y={PlotOffset} width={PlotSize} height={PlotSize} style={{ fill: 'transparent', cursor: 'crosshair' }}
|
||||
ref={interactivityRect as any}
|
||||
onMouseMove={(ev) => {
|
||||
interactivity.next({ ...interactivity.value, inside: true });
|
||||
ev.currentTarget.style.stroke = 'black';
|
||||
ev.currentTarget.style.strokeWidth = '4px';
|
||||
}}
|
||||
onMouseDown={(ev) => {
|
||||
interactivity.next({ ...interactivity.value, mouseDown: true, boxStart: getPlotMouseOffset(ev) });
|
||||
}}
|
||||
onMouseLeave={(ev) => {
|
||||
interactivity.next({ ...interactivity.value, inside: false, crosshairOffset: undefined });
|
||||
ev.currentTarget.style.stroke = '#333';
|
||||
ev.currentTarget.style.strokeWidth = '1px';
|
||||
}} />
|
||||
<PlotInteractivity drawing={drawing} interactity={interactivity} />
|
||||
</svg>
|
||||
</div>;
|
||||
}, (prev, next) => prev.model === next.model && prev.pairwiseMetric === next.pairwiseMetric);
|
||||
|
||||
function PlotInteractivity({ drawing, interactity }: { drawing: MAPairwiseMetricDrawing, interactity: BehaviorSubject<PlotInteractivityState> }) {
|
||||
const state = useBehavior(interactity);
|
||||
const { crosshairOffset, inside } = state;
|
||||
const box = getBox(state);
|
||||
const label = getCrosshairLabel(state);
|
||||
|
||||
let labelNode: ReactNode | undefined;
|
||||
if (label) {
|
||||
const labelStyle: CSSProperties | undefined = label ? { fontSize: '45px', fill: 'black', fontWeight: 'bold', pointerEvents: 'none', userSelect: 'none' } : undefined;
|
||||
let x: number, y: number, anchor: string;
|
||||
if (crosshairOffset![0] < PlotSize / 2) {
|
||||
x = PlotOffset + crosshairOffset![0] + 20;
|
||||
anchor = 'start';
|
||||
} else {
|
||||
x = PlotOffset + crosshairOffset![0] - 20;
|
||||
anchor = 'end';
|
||||
}
|
||||
|
||||
if (crosshairOffset![1] < PlotSize / 2) {
|
||||
y = PlotOffset + crosshairOffset![1] + 65;
|
||||
} else {
|
||||
y = PlotOffset + crosshairOffset![1] - (label[2] ? 3 * 45 : 2 * 45) + 20;
|
||||
}
|
||||
|
||||
labelNode = <text y={y} style={labelStyle} textAnchor={anchor}>
|
||||
<tspan x={x}>S: {label[0]}</tspan>
|
||||
<tspan x={x} dy={45}>A: {label[1]}</tspan>
|
||||
{label[2] && <tspan x={x} dy={45}>{label[2]}</tspan>}
|
||||
</text>;
|
||||
}
|
||||
|
||||
return <>
|
||||
{inside && crosshairOffset && <line x1={crosshairOffset[0] + PlotOffset} x2={crosshairOffset[0] + PlotOffset} y1={PlotOffset} y2={PlotOffset + PlotSize} style={{ pointerEvents: 'none', stroke: 'black', strokeDasharray: '5,5' }} />}
|
||||
{inside && crosshairOffset && <line x1={PlotOffset} x2={PlotOffset + PlotSize} y1={crosshairOffset[1] + PlotOffset} y2={crosshairOffset[1] + PlotOffset} style={{ pointerEvents: 'none', stroke: 'black', strokeDasharray: '5,5' }} />}
|
||||
{box && <rect x={PlotOffset + box[0]} y={PlotOffset + box[1]} width={box[2]} height={box[3]} style={{ stroke: '#eee', strokeWidth: 4, fill: 'rgba(0, 0, 0, 0.15)', pointerEvents: 'none' }} />}
|
||||
{labelNode}
|
||||
</>;
|
||||
}
|
||||
|
||||
function getCrosshairLabel(state: PlotInteractivityState) {
|
||||
if (!state.drawing || !state.crosshairOffset || !state.inside) return;
|
||||
|
||||
const { drawing } = state;
|
||||
const rA = getResidueIndex(drawing, clamp(state.crosshairOffset[0], 0, PlotSize));
|
||||
const rB = getResidueIndex(drawing, clamp(state.crosshairOffset[1], 0, PlotSize));
|
||||
|
||||
const value = drawing.metric.values[rA]?.[rB] ?? drawing.metric.values[rB]?.[rA];
|
||||
const valueLabel = typeof value === 'number' ? `${round(value, 2)} Å` : '';
|
||||
|
||||
return [getResidueLabel(drawing, rA), getResidueLabel(drawing, rB), valueLabel];
|
||||
}
|
||||
|
||||
function getResidueIndex(drawing: MAPairwiseMetricDrawing, offset: number) {
|
||||
const rI = drawing.metric.residueRange[0] + Math.round(offset / PlotSize * (drawing.metric.residueRange[1] - drawing.metric.residueRange[0] + 1)) as ResidueIndex;
|
||||
return clamp(rI, drawing.metric.residueRange[0], drawing.metric.residueRange[1]) as ResidueIndex;
|
||||
}
|
||||
|
||||
function getResidueLabel(drawing: MAPairwiseMetricDrawing, rI: ResidueIndex) {
|
||||
const hierarchy = drawing.model.atomicHierarchy;
|
||||
const asym_id = hierarchy.chains.label_asym_id;
|
||||
const seq_id = hierarchy.residues.label_seq_id;
|
||||
const comp_id = hierarchy.atoms.label_comp_id;
|
||||
|
||||
return `${asym_id.value(AtomicHierarchy.residueChainIndex(hierarchy, rI))} ${seq_id.value(rI)} ${comp_id.value(AtomicHierarchy.residueFirstAtomIndex(hierarchy, rI))}`;
|
||||
}
|
||||
|
||||
function getBox(state: PlotInteractivityState) {
|
||||
const start = state.boxStart;
|
||||
const end = state.mouseDown ? state.crosshairOffset : state.boxEnd;
|
||||
if (!start || !end) return undefined;
|
||||
|
||||
const x = clamp(Math.min(start[0], end[0]), 0, PlotSize);
|
||||
const width = clamp(Math.max(start[0], end[0]), 0, PlotSize) - x;
|
||||
const y = clamp(Math.min(start[1], end[1]), 0, PlotSize);
|
||||
const height = clamp(Math.max(start[1], end[1]), 0, PlotSize) - y;
|
||||
|
||||
if (width < 1 && height < 1) return undefined;
|
||||
|
||||
return [x, y, width, height];
|
||||
}
|
||||
|
||||
function getPlotMouseOffset(ev: React.MouseEvent<SVGRectElement, MouseEvent>) {
|
||||
return getPlotMouseOffsetBase(ev.currentTarget, ev.clientX, ev.clientY);
|
||||
}
|
||||
|
||||
function getPlotMouseOffsetBase(target: HTMLElement | SVGRectElement, clientX: number, clientY: number) {
|
||||
const rect = target.getBoundingClientRect();
|
||||
const offsetX = PlotSize * (clientX - rect.left) / rect.width;
|
||||
const offsetY = PlotSize * (clientY - rect.top) / rect.height;
|
||||
return [offsetX, offsetY] as [number, number];
|
||||
}
|
||||
|
||||
function findModelRef(plugin: PluginContext, model: Model | undefined) {
|
||||
if (!model) return undefined;
|
||||
for (const m of plugin.managers.structure.hierarchy.current.models) {
|
||||
if (m.cell.obj?.data === model) return m;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function highlightState(plugin: PluginContext, state: PlotInteractivityState) {
|
||||
const structure = findModelRef(plugin, state.model)?.structures[0]?.cell.obj?.data;
|
||||
if (!state.drawing || !state.crosshairOffset || !state.inside || !structure) {
|
||||
plugin.managers.interactivity.lociHighlights.clearHighlights();
|
||||
return;
|
||||
}
|
||||
|
||||
const { drawing } = state;
|
||||
const rA = getResidueIndex(drawing, clamp(state.crosshairOffset[0], 0, PlotSize));
|
||||
const rB = getResidueIndex(drawing, clamp(state.crosshairOffset[1], 0, PlotSize));
|
||||
|
||||
const resIdx = StructureProperties.residue.key;
|
||||
const loci = StructureQuery.loci(atoms({
|
||||
residueTest: ctx => {
|
||||
const rI = resIdx(ctx.element);
|
||||
return rI === rA || rI === rB;
|
||||
},
|
||||
}), structure);
|
||||
|
||||
plugin.managers.interactivity.lociHighlights.highlightOnly({ loci });
|
||||
}
|
||||
|
||||
async function overpaintState(plugin: PluginContext, state: PlotInteractivityState) {
|
||||
const tag = 'modelarchive-pae-overpaint';
|
||||
|
||||
const overpaints = plugin.state.data.selectQ(q => q.root.subtree().withTag(tag));
|
||||
const update = plugin.build();
|
||||
for (const overpaint of overpaints) update.delete(overpaint);
|
||||
|
||||
const model = findModelRef(plugin, state.model);
|
||||
const structure = model?.structures[0]?.cell.obj?.data;
|
||||
if (!state.drawing || !state.boxStart || !(state.boxEnd || state.crosshairOffset) || !structure) {
|
||||
if (!overpaints) return;
|
||||
return reApplyRepresentationStates(plugin, update);
|
||||
}
|
||||
|
||||
const start = state.boxStart;
|
||||
const end = state.mouseDown ? state.crosshairOffset! : state.boxEnd!;
|
||||
|
||||
const x0 = clamp(Math.min(start[0], end[0]), 0, PlotSize);
|
||||
const x1 = clamp(Math.max(start[0], end[0]), 0, PlotSize);
|
||||
const y0 = clamp(Math.min(start[1], end[1]), 0, PlotSize);
|
||||
const y1 = clamp(Math.max(start[1], end[1]), 0, PlotSize);
|
||||
|
||||
if (x1 - x0 <= 1 || y1 - y0 <= 1) {
|
||||
if (!overpaints) return;
|
||||
return reApplyRepresentationStates(plugin, update);
|
||||
}
|
||||
|
||||
const representations = plugin.state.data.selectQ(q =>
|
||||
q.byRef(model.cell.transform.ref!)
|
||||
.subtree()
|
||||
.ofType(PluginStateObject.Molecule.Structure.Representation3D)
|
||||
);
|
||||
|
||||
const resIdx = StructureProperties.residue.key;
|
||||
|
||||
const startScored = getResidueIndex(state.drawing, x0);
|
||||
const endScored = getResidueIndex(state.drawing, x1);
|
||||
const lociScored = StructureQuery.loci(atoms({
|
||||
residueTest: ctx => {
|
||||
const rI = resIdx(ctx.element);
|
||||
return rI >= startScored && rI <= endScored;
|
||||
},
|
||||
}), structure);
|
||||
|
||||
const startAligned = getResidueIndex(state.drawing, y0);
|
||||
const endAligned = getResidueIndex(state.drawing, y1);
|
||||
const lociAligned = StructureQuery.loci(atoms({
|
||||
residueTest: ctx => {
|
||||
const rI = resIdx(ctx.element);
|
||||
return rI >= startAligned && rI <= endAligned;
|
||||
},
|
||||
}), structure);
|
||||
|
||||
const layers = [{
|
||||
bundle: StructureElement.Bundle.fromSubStructure(structure, structure),
|
||||
color: Color(0x777777),
|
||||
clear: false,
|
||||
}, {
|
||||
bundle: StructureElement.Bundle.fromLoci(lociScored),
|
||||
color: PlotColors.ScoredOverpaint,
|
||||
clear: false,
|
||||
}, {
|
||||
bundle: StructureElement.Bundle.fromLoci(lociAligned),
|
||||
color: PlotColors.AlignedOverpaint,
|
||||
clear: false,
|
||||
}];
|
||||
|
||||
for (const repr of representations) {
|
||||
update.to(repr).apply(OverpaintStructureRepresentation3DFromBundle, { layers }, { tags: [tag], state: { isGhost: true } });
|
||||
}
|
||||
|
||||
return update.commit();
|
||||
}
|
||||
|
||||
async function reApplyRepresentationStates(plugin: PluginContext, update: StateBuilder.Root) {
|
||||
await update.commit();
|
||||
const states = plugin.state.data.selectQ(q => q.root.subtree().ofType(PluginStateObject.Molecule.Structure.Representation3DState));
|
||||
for (const state of states) {
|
||||
const data = state.obj?.data;
|
||||
if (!data) continue;
|
||||
data.repr.setState(data.state);
|
||||
plugin.canvas3d?.update(data.repr);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,24 @@
|
||||
/**
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2021-24 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { Unit } from '../../../mol-model/structure';
|
||||
import { CustomProperty } from '../../../mol-model-props/common/custom-property';
|
||||
import { CifFrame } from '../../../mol-io/reader/cif';
|
||||
import { toDatabase } from '../../../mol-io/reader/cif/schema';
|
||||
import { mmCIF_Schema } from '../../../mol-io/reader/cif/schema/mmcif';
|
||||
import { MmcifFormat } from '../../../mol-model-formats/structure/mmcif';
|
||||
import { CustomModelProperty } from '../../../mol-model-props/common/custom-model-property';
|
||||
import { CustomProperty } from '../../../mol-model-props/common/custom-property';
|
||||
import { CustomPropertyDescriptor } from '../../../mol-model/custom-property';
|
||||
import { Unit } from '../../../mol-model/structure';
|
||||
import { Model, ResidueIndex } from '../../../mol-model/structure/model';
|
||||
import { QuerySymbolRuntime } from '../../../mol-script/runtime/query/compiler';
|
||||
import { AtomicIndex } from '../../../mol-model/structure/model/properties/atomic';
|
||||
import { CustomPropSymbol } from '../../../mol-script/language/symbol';
|
||||
import { Type } from '../../../mol-script/language/type';
|
||||
import { CustomPropertyDescriptor } from '../../../mol-model/custom-property';
|
||||
import { MmcifFormat } from '../../../mol-model-formats/structure/mmcif';
|
||||
import { AtomicIndex } from '../../../mol-model/structure/model/properties/atomic';
|
||||
import { QuerySymbolRuntime } from '../../../mol-script/runtime/query/compiler';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
|
||||
export { QualityAssessment };
|
||||
|
||||
@@ -26,10 +29,19 @@ interface QualityAssessment {
|
||||
}
|
||||
|
||||
namespace QualityAssessment {
|
||||
export interface Pairwise {
|
||||
id: number
|
||||
name: string
|
||||
|
||||
residueRange: [ResidueIndex, ResidueIndex]
|
||||
valueRange: [number, number]
|
||||
values: Record<ResidueIndex, Record<ResidueIndex, number | undefined> | undefined>
|
||||
}
|
||||
|
||||
const Empty = {
|
||||
value: {
|
||||
localMetrics: new Map()
|
||||
}
|
||||
localMetrics: new Map(),
|
||||
} satisfies QualityAssessment
|
||||
};
|
||||
|
||||
export function isApplicable(model?: Model, localMetricName?: 'pLDDT' | 'qmean'): boolean {
|
||||
@@ -106,6 +118,101 @@ namespace QualityAssessment {
|
||||
};
|
||||
}
|
||||
|
||||
const PairwiseSchema = {
|
||||
ma_qa_metric: mmCIF_Schema.ma_qa_metric,
|
||||
ma_qa_metric_local_pairwise: mmCIF_Schema.ma_qa_metric_local_pairwise
|
||||
};
|
||||
|
||||
export function findModelArchiveCIFPAEMetrics(frame: CifFrame) {
|
||||
const { ma_qa_metric, ma_qa_metric_local_pairwise } = toDatabase(PairwiseSchema, frame);
|
||||
const result: { id: number, name: string }[] = [];
|
||||
if (ma_qa_metric_local_pairwise._rowCount === 0) return result;
|
||||
|
||||
for (let i = 0, il = ma_qa_metric._rowCount; i < il; i++) {
|
||||
if (ma_qa_metric.mode.value(i) !== 'local-pairwise') continue;
|
||||
const id = ma_qa_metric.id.value(i);
|
||||
const name = ma_qa_metric.name.value(i);
|
||||
if (!name.toLowerCase().includes('pae')) continue;
|
||||
result.push({ id, name });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function pairwiseMetricFromModelArchiveCIF(model: Model, frame: CifFrame, metricId: number): Pairwise | undefined {
|
||||
const db = toDatabase(PairwiseSchema, frame);
|
||||
if (!db.ma_qa_metric_local_pairwise._rowCount) return undefined;
|
||||
|
||||
const { ma_qa_metric, ma_qa_metric_local_pairwise } = db;
|
||||
const { model_id, label_asym_id_1, label_seq_id_1, label_asym_id_2, label_seq_id_2, metric_id, metric_value } = db.ma_qa_metric_local_pairwise;
|
||||
const { index } = model.atomicHierarchy;
|
||||
|
||||
let metric: Pairwise | undefined;
|
||||
|
||||
for (let i = 0, il = ma_qa_metric._rowCount; i < il; i++) {
|
||||
if (ma_qa_metric.mode.value(i) !== 'local-pairwise') continue;
|
||||
const id = ma_qa_metric.id.value(i);
|
||||
if (id !== metricId) continue;
|
||||
|
||||
const name = ma_qa_metric.name.value(i);
|
||||
metric = {
|
||||
id,
|
||||
name,
|
||||
residueRange: [Number.MAX_SAFE_INTEGER as ResidueIndex, Number.MIN_SAFE_INTEGER as ResidueIndex],
|
||||
valueRange: [Number.MAX_VALUE, -Number.MAX_VALUE],
|
||||
values: {}
|
||||
};
|
||||
}
|
||||
|
||||
if (!metric) return undefined;
|
||||
|
||||
const { values, residueRange, valueRange } = metric;
|
||||
const residueKey: AtomicIndex.ResidueLabelKey = {
|
||||
label_entity_id: '',
|
||||
label_asym_id: '',
|
||||
label_seq_id: 0,
|
||||
pdbx_PDB_ins_code: undefined,
|
||||
};
|
||||
|
||||
for (let i = 0, il = ma_qa_metric_local_pairwise._rowCount; i < il; i++) {
|
||||
if (model_id.value(i) !== model.modelNum || metric_id.value(i) !== metricId) continue;
|
||||
|
||||
let labelAsymId = label_asym_id_1.value(i);
|
||||
let entityIndex = index.findEntity(labelAsymId);
|
||||
residueKey.label_entity_id = model.entities.data.id.value(entityIndex);
|
||||
residueKey.label_asym_id = labelAsymId;
|
||||
residueKey.label_seq_id = label_seq_id_1.value(i);
|
||||
|
||||
const rI_1 = index.findResidueLabel(residueKey);
|
||||
if (rI_1 < 0) continue;
|
||||
|
||||
labelAsymId = label_asym_id_2.value(i);
|
||||
entityIndex = index.findEntity(labelAsymId);
|
||||
residueKey.label_entity_id = model.entities.data.id.value(entityIndex);
|
||||
residueKey.label_asym_id = labelAsymId;
|
||||
residueKey.label_seq_id = label_seq_id_2.value(i);
|
||||
|
||||
const rI_2 = index.findResidueLabel(residueKey);
|
||||
if (rI_1 < 0) continue;
|
||||
|
||||
let r1 = values[rI_1];
|
||||
if (!r1) {
|
||||
r1 = {};
|
||||
values[rI_1] = r1;
|
||||
}
|
||||
const value = metric_value.value(i);
|
||||
r1[rI_2] = value;
|
||||
|
||||
if (rI_1 < residueRange[0]) residueRange[0] = rI_1;
|
||||
if (rI_2 < residueRange[0]) residueRange[0] = rI_2;
|
||||
if (rI_1 > residueRange[1]) residueRange[1] = rI_1;
|
||||
if (rI_2 > residueRange[1]) residueRange[1] = rI_2;
|
||||
if (value < valueRange[0]) valueRange[0] = value;
|
||||
if (value > valueRange[1]) valueRange[1] = value;
|
||||
}
|
||||
|
||||
return metric;
|
||||
}
|
||||
|
||||
export const symbols = {
|
||||
pLDDT: QuerySymbolRuntime.Dynamic(CustomPropSymbol('ma', 'quality-assessment.pLDDT', Type.Num),
|
||||
ctx => {
|
||||
|
||||
@@ -5216,6 +5216,70 @@ export const mmCIF_Schema = {
|
||||
*/
|
||||
metric_value: float,
|
||||
},
|
||||
ma_qa_metric_local_pairwise: {
|
||||
/**
|
||||
* A unique identifier for the category.
|
||||
*/
|
||||
ordinal_id: int,
|
||||
/**
|
||||
* The identifier for the structural model, for which local QA metric is provided.
|
||||
* This data item is a pointer to _ma_model_list.model_id
|
||||
* in the MA_MODEL_LIST category.
|
||||
*/
|
||||
model_id: int,
|
||||
/**
|
||||
* The identifier for the asym id of the residue in the
|
||||
* structural model, for which local QA metric is provided.
|
||||
* This data item is a pointer to _atom_site.label_asym_id
|
||||
* in the ATOM_SITE category.
|
||||
*/
|
||||
label_asym_id_1: str,
|
||||
/**
|
||||
* The identifier for the sequence index of the residue
|
||||
* in the structural model, for which local QA metric is provided.
|
||||
* This data item is a pointer to _atom_site.label_seq_id
|
||||
* in the ATOM_SITE category.
|
||||
*/
|
||||
label_seq_id_1: int,
|
||||
/**
|
||||
* The component identifier for the residue in the
|
||||
* structural model, for which local QA metric is provided.
|
||||
* This data item is a pointer to _atom_site.label_comp_id
|
||||
* in the ATOM_SITE category.
|
||||
*/
|
||||
label_comp_id_1: str,
|
||||
/**
|
||||
* The identifier for the asym id of the residue in the
|
||||
* structural model, for which local QA metric is provided.
|
||||
* This data item is a pointer to _atom_site.label_asym_id
|
||||
* in the ATOM_SITE category.
|
||||
*/
|
||||
label_asym_id_2: str,
|
||||
/**
|
||||
* The identifier for the sequence index of the residue
|
||||
* in the structural model, for which local QA metric is provided.
|
||||
* This data item is a pointer to _atom_site.label_seq_id
|
||||
* in the ATOM_SITE category.
|
||||
*/
|
||||
label_seq_id_2: int,
|
||||
/**
|
||||
* The component identifier for the residue in the
|
||||
* structural model, for which local QA metric is provided.
|
||||
* This data item is a pointer to _atom_site.label_comp_id
|
||||
* in the ATOM_SITE category.
|
||||
*/
|
||||
label_comp_id_2: str,
|
||||
/**
|
||||
* The identifier for the QA metric.
|
||||
* This data item is a pointer to _ma_qa_metric.id in the
|
||||
* MA_QA_METRIC category.
|
||||
*/
|
||||
metric_id: int,
|
||||
/**
|
||||
* The value of the local QA metric.
|
||||
*/
|
||||
metric_value: float,
|
||||
},
|
||||
};
|
||||
|
||||
export type mmCIF_Schema = typeof mmCIF_Schema;
|
||||
|
||||
@@ -263,4 +263,16 @@ export namespace AtomicHierarchy {
|
||||
export function chainResidueCount(segs: AtomicSegments, cI: ChainIndex) {
|
||||
return chainEndResidueIndexExcl(segs, cI) - chainStartResidueIndex(segs, cI);
|
||||
}
|
||||
|
||||
export function residueFirstAtomIndex(hierarchy: AtomicHierarchy, rI: ResidueIndex) {
|
||||
return hierarchy.residueAtomSegments.offsets[rI];
|
||||
}
|
||||
|
||||
export function atomChainIndex(hierarchy: AtomicHierarchy, eI: ElementIndex) {
|
||||
return hierarchy.chainAtomSegments.index[eI];
|
||||
}
|
||||
|
||||
export function residueChainIndex(hierarchy: AtomicHierarchy, rI: ResidueIndex) {
|
||||
return hierarchy.chainAtomSegments.index[hierarchy.residueAtomSegments.offsets[rI]];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2017-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
@@ -13,6 +13,11 @@ namespace StructureQuery {
|
||||
export function run(query: StructureQuery, structure: Structure, options?: QueryContextOptions) {
|
||||
return query(new QueryContext(structure, options));
|
||||
}
|
||||
|
||||
export function loci(query: StructureQuery, structure: Structure, options?: QueryContextOptions) {
|
||||
const sel = query(new QueryContext(structure, options));
|
||||
return StructureSelection.toLociWithSourceUnits(sel);
|
||||
}
|
||||
}
|
||||
|
||||
export { StructureQuery };
|
||||
@@ -80,7 +80,7 @@ export type CollapsableState = {
|
||||
}
|
||||
|
||||
export abstract class CollapsableControls<P = {}, S = {}, SS = {}> extends PluginUIComponent<P & CollapsableProps, S & CollapsableState, SS> {
|
||||
toggleCollapsed = () => {
|
||||
toggleCollapsed() {
|
||||
this.setState({ isCollapsed: !this.state.isCollapsed } as (S & CollapsableState));
|
||||
};
|
||||
|
||||
@@ -102,7 +102,7 @@ export abstract class CollapsableControls<P = {}, S = {}, SS = {}> extends Plugi
|
||||
|
||||
return <div className={wrapClass}>
|
||||
<div id={divid} className='msp-transform-header'>
|
||||
<Button icon={this.state.brand ? void 0 : this.state.isCollapsed ? ArrowRightSvg : ArrowDropDownSvg} noOverflow onClick={this.toggleCollapsed}
|
||||
<Button icon={this.state.brand ? void 0 : this.state.isCollapsed ? ArrowRightSvg : ArrowDropDownSvg} noOverflow onClick={() => this.toggleCollapsed()}
|
||||
className={this.state.brand ? `msp-transform-header-brand msp-transform-header-brand-${this.state.brand.accent}` : void 0} title={`Click to ${this.state.isCollapsed ? 'expand' : 'collapse'}`}>
|
||||
{/* {this.state.brand && <div className={`msp-accent-bg-${this.state.brand.accent}`}>{this.state.brand.svg ? <Icon svg={this.state.brand.svg} /> : this.state.brand.name}</div>} */}
|
||||
<Icon svg={this.state.brand?.svg} inline />
|
||||
|
||||
@@ -34,6 +34,10 @@
|
||||
margin: $control-spacing;
|
||||
}
|
||||
|
||||
.msp-svg-text {
|
||||
fill: $font-color;
|
||||
}
|
||||
|
||||
& {
|
||||
background: $default-background;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@use "sass:color";
|
||||
|
||||
$default-background: #2D3E50;
|
||||
$font-color: #EDF1F2;
|
||||
$hover-font-color: #3B9AD9;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@use "sass:color";
|
||||
|
||||
$default-background: #111318;
|
||||
$font-color: #ccd4e0;
|
||||
$hover-font-color: #51A2FB;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const { createApp, createExample, createBrowserTest } = require('./webpack.config.common.js');
|
||||
|
||||
const examples = ['proteopedia-wrapper', 'basic-wrapper', 'lighting', 'alpha-orbitals'];
|
||||
const examples = ['proteopedia-wrapper', 'basic-wrapper', 'lighting', 'alpha-orbitals', 'alphafolddb-pae'];
|
||||
const tests = [
|
||||
'font-atlas',
|
||||
'marching-cubes',
|
||||
|
||||
Reference in New Issue
Block a user