Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@

## Interface and handling

- explain all browser input, CopomuS ranking and candidate-filter controls with
tooltips; annotate compensatory mutations in one vaRRI view and compare all
four-state energies in an adjacent Plotly chart (#313)

- isolate real-browser numerical fixtures from editable GUI examples and keep
CSV export checks synchronized with selected columns and seed-end defaults

Expand Down Expand Up @@ -337,6 +341,20 @@ energies, restricted partition sums, and trackers.
################################################################################
################################################################################

261027 Alexander Mitrofanov
* gui/{index.html,app.js,options.js,copomus-options.js,styles.css}
+ Add explanatory tooltips to every input label and all CopomuS ranking
measures and candidate filters; set both FASTA input minima to 400 pixels.
* gui/{copomus.js,visualization.js,index.html,styles.css}, gui/tests/,
scripts/wasm/test-browser.py
* Replace the four selectable mutation-state viewers with one wild-type vaRRI
view that annotates both designed substitutions in dark green at their
original signed coordinates.
+ Plot the ww, wm, mw and mm interaction energies beside vaRRI, including a
dotted blue wild-type baseline and responsive stacked layout (issue #313).
* README.md, gui/docs/copomus.md
* Document the annotated single-view and four-state energy comparison.

261026 Alexander Mitrofanov
* scripts/wasm/test-browser.py
* Stop loading editable GUI examples as numerical browser-test fixtures; seed
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ seeding of interaction sites**

Predict short RNA pairs, evaluate supplied structures or rank compensatory mutations locally with the [browser interface](https://backofenlab.github.io/IntaRNA/gui/).
The browser accepts up to 50 records per strand, 2–5,000 nucleotides each, with longest selected query × longest selected target at most 1,000,000. Prediction defaults to 10 interactions per pair and includes opening and seed energies; evaluation selects all energy columns.
Mutation Design accepts one unambiguous query/target pair, runs CopomuS in a self-hosted Pyodide worker, and uses the same IntaRNA WebAssembly engine for every wildtype, single-mutant and compensatory prediction. Its extended table and vaRRI controls compare all four mutation states.
Mutation Design accepts one unambiguous query/target pair, runs CopomuS in a self-hosted Pyodide worker, and uses the same IntaRNA WebAssembly engine for every wildtype, single-mutant and compensatory prediction. Its extended table links one annotated vaRRI view with a four-state interaction-energy chart.
Computed runs stay in separate, foldable sessions under Results in the current tab. Name and select sessions to restore their inputs and compare results; editing or resetting parameters keeps earlier runs.
Select an interaction to view it in vaRRI, or download a session’s ZIP with inputs, compiled versions, reproducible parameters and all outputs. Sessions are temporary and are cleared when the page is reloaded.
Minimum-energy and interaction-probability output files include local Plotly visualizations with PNG export; every additional file retains its original text view and download.
Expand Down
13 changes: 8 additions & 5 deletions gui/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ function element(tag, text, className) {
return node;
}
const copomusMeasureInputs = [], copomusMeasureRows = new Map();
for (const [key, labelText] of COPOMUS_MEASURES) {
const input = checkbox(byId('copomus-measures'), key, labelText), label = input.parentElement;
for (const [key, labelText, description] of COPOMUS_MEASURES) {
const input = checkbox(byId('copomus-measures'), key, labelText, description), label = input.parentElement;
const row = element('div', '', 'measure-choice'), actions = element('span', '', 'measure-priority');
label.replaceWith(row); row.append(label, actions); copomusMeasureRows.set(key, row); copomusMeasureInputs.push(input);
for (const [text, direction] of [['↑', -1], ['↓', 1]]) {
Expand All @@ -42,7 +42,7 @@ for (const [key, labelText] of COPOMUS_MEASURES) {
actions.append(button);
}
}
const copomusFilterInputs = COPOMUS_FILTERS.map(([key, label]) => checkbox(byId('copomus-filters'), key, label));
const copomusFilterInputs = COPOMUS_FILTERS.map(([key, label, description]) => checkbox(byId('copomus-filters'), key, label, description));
function orderCopomusMeasures(selected) {
const order = [...selected, ...COPOMUS_MEASURES.map(([key]) => key).filter((key) => !selected.includes(key))];
for (const key of order) byId('copomus-measures').append(copomusMeasureRows.get(key));
Expand Down Expand Up @@ -79,9 +79,10 @@ byId('parameter-tabs').addEventListener('keydown', (event) => {
// labels do not push neighboring controls out of alignment.
function renderField(field, id = field.key) {
const wrapper = element('div', '', `field${field.type === 'checkbox' ? ' check' : ''}`);
const label = element('label', field.label); label.htmlFor = id; label.title = `--${field.key}`;
const label = element('label', field.label); label.htmlFor = id; label.title = field.help;
const input = element(field.type === 'select' ? 'select' : field.type === 'textarea' ? 'textarea' : 'input'); input.id = id;
if (input.tagName === 'INPUT') input.type = field.type;
input.title = field.help;
if (field.type === 'select') for (const [value, name] of Object.entries(field.choices)) { const option = element('option', name); option.value = value; input.append(option); }
if (field.type === 'number') { input.min = field.min; input.max = field.max; input.step = field.step ?? 1; }
if (field.type === 'checkbox') { input.checked = field.value; wrapper.append(input, label); }
Expand Down Expand Up @@ -116,7 +117,9 @@ for (const definition of GROUPS) {
const primary = element('div', '', 'seed-primary-row');
const wrapper = element('div', '', 'field seed-mode');
const label = element('label', 'Seed constraint'); label.htmlFor = 'seed-mode';
label.title = 'Chooses whether prediction uses no seed, one or more explicitly encoded seeds, or automatically identified seed interactions.';
const select = element('select'); select.id = 'seed-mode';
select.title = label.title;
for (const [value, name] of [['none', 'Disable seed constraint'], ['explicit', 'Explicit seed'], ['identify', 'Identify seeds']]) {
const option = element('option', name); option.value = value; select.append(option);
}
Expand Down Expand Up @@ -146,7 +149,7 @@ for (const definition of GROUPS) {
}
function checkbox(container, key, labelText, description) {
const label = element('label'), input = element('input'); input.type = 'checkbox'; input.value = key;
if (description) label.title = description;
if (description) { label.title = description; input.title = description; }
label.append(input, document.createTextNode(labelText)); container.append(label); return input;
}
const csvBody = byId('csv-options').querySelector('.group-body');
Expand Down
22 changes: 14 additions & 8 deletions gui/copomus-options.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
export const COPOMUS_MEASURES = Object.freeze([
['mfeCover', 'MFE coverage'], ['E', 'Interaction energy'],
['minDeltaE', 'Minimum energy difference'], ['EDqi', 'Query accessibility'],
['cEDqi', 'Constant query accessibility'], ['Eqi', 'Query energy profile'],
['Eti', 'Target energy profile'],
['mfeCover', 'MFE coverage', 'Prefers mutations whose designed positions lie inside both the wild-type and compensatory minimum-free-energy interactions.'],
['E', 'Interaction energy', 'Checks that both matching pairs interact favorably while each single mutant weakens the interaction by the configured separation threshold.'],
['minDeltaE', 'Minimum energy difference', 'Ranks by the smallest energy separation between a matching pair and either single-mutant combination.'],
['EDqi', 'Query accessibility', 'Prefers mutations whose compensatory query nucleotide is less accessible than its wild-type counterpart.'],
['cEDqi', 'Constant query accessibility', 'Prefers mutations that preserve the query nucleotide accessibility within one kcal/mol.'],
['Eqi', 'Query energy profile', 'Checks the interaction-energy profile at the mutated query position across all four sequence combinations.'],
['Eti', 'Target energy profile', 'Checks the interaction-energy profile at the mutated target position across all four sequence combinations.'],
]);
export const COPOMUS_FILTERS = Object.freeze([
['GU', 'Exclude GU/UG pairs'], ['AU', 'Exclude AU/UA pairs'],
['GC', 'Exclude GC/CG pairs'], ['lp', 'Exclude unstackable lonely pairs'],
['lpMfe', 'Exclude lonely pairs in the MFE'], ['he', 'Exclude potential helix ends'],
['heMfe', 'Exclude helix ends in the MFE'],
['GU', 'Exclude GU/UG pairs', 'Removes candidate positions whose wild-type base pair is GU or UG.'],
['AU', 'Exclude AU/UA pairs', 'Removes candidate positions whose wild-type base pair is AU or UA.'],
['GC', 'Exclude GC/CG pairs', 'Removes candidate positions whose wild-type base pair is GC or CG.'],
['lp', 'Exclude unstackable lonely pairs', 'Removes candidates that cannot stack with a neighboring canonical base pair in the input sequences.'],
['lpMfe', 'Exclude lonely pairs in the MFE', 'Removes candidates that form a lonely base pair in the wild-type minimum-free-energy interaction.'],
['he', 'Exclude potential helix ends', 'Removes candidates that could occur at either end of an interaction helix.'],
['heMfe', 'Exclude helix ends in the MFE', 'Removes candidates located at a helix end in the wild-type minimum-free-energy interaction.'],
]);
export const COPOMUS_DEFAULTS = Object.freeze({
measures: ['mfeCover', 'E', 'minDeltaE'],
Expand Down
88 changes: 60 additions & 28 deletions gui/copomus.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,54 +3,86 @@ import { InteractionVisualization } from './visualization.js';
export { COPOMUS_MEASURES, COPOMUS_FILTERS, COPOMUS_DEFAULTS, normalizeCopomus } from './copomus-options.js';

const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
const MUTATION_COLOR = '006400';
const ENERGY_STATES = Object.freeze(['ww', 'wm', 'mw', 'mm']);

const MODE_LABELS = Object.freeze({
ww: 'Wild type', wm: 'Target mutant', mw: 'Query mutant', mm: 'Compensatory mutant',
});
function numeric(value) {
return typeof value === 'number' && Number.isFinite(value) ? value : null;
}

export function mutationAnnotations(candidate) {
const queryPosition = Number(candidate.values.qIndex), targetPosition = Number(candidate.values.tIndex);
const mutated = String(candidate.values.bpMutated ?? '');
if (!Number.isInteger(queryPosition) || queryPosition === 0 || !Number.isInteger(targetPosition) || targetPosition === 0 || !/^[ACGU]{2}$/.test(mutated)) throw new Error('The selected mutation has invalid sequence coordinates or bases.');
// vaRRI receives native sequence 1 (target) before sequence 2 (query), while
// CopomuS stores each mutation and base pair in query-first order.
return `1:${targetPosition}${mutated[1]}:${MUTATION_COLOR},2:${queryPosition}${mutated[0]}:${MUTATION_COLOR}`;
}

export function mutationEnergyPlot(candidate) {
const energies = ENERGY_STATES.map((state) => numeric(candidate.interactions[state]?.E));
const wildtype = energies[0];
return {
data: [{
type: 'bar', x: [...ENERGY_STATES], y: energies,
marker: { color: '#79a9cc', line: { color: '#145a96', width: 1 } },
hovertemplate: '%{x}: %{y:.2f} kcal/mol<extra></extra>',
}],
layout: {
autosize: true, height: 398, margin: { l: 58, r: 14, t: 18, b: 48 },
showlegend: false, paper_bgcolor: '#ffffff', plot_bgcolor: '#ffffff',
font: { family: '"Segoe UI", Tahoma, Geneva, sans-serif', color: '#1c2b39', size: 12 },
xaxis: { type: 'category', fixedrange: true },
yaxis: { title: { text: 'E [kcal/mol]' }, fixedrange: true, zerolinecolor: '#ccd9e5', gridcolor: '#e0e8f0' },
shapes: wildtype === null ? [] : [{
type: 'line', xref: 'paper', x0: 0, x1: 1, yref: 'y', y0: wildtype, y1: wildtype,
line: { color: '#145a96', width: 2, dash: 'dot' },
}],
},
config: { displayModeBar: false, responsive: true, staticPlot: false },
};
}

class MutationVisualization {
constructor(root) {
this.root = root;
this.viewer = new InteractionVisualization(root, 'mutation-');
this.controls = root.querySelector('[data-role=mutation-modes]');
this.title = root.querySelector('[data-role=mutation-visualization-title]');
for (const button of this.controls.querySelectorAll('button')) button.addEventListener('click', () => this.showMode(button.value));
this.plot = root.querySelector('[data-role=mutation-energy-plot]');
}

clear() {
this.candidate = null; this.request = null; this.controls.hidden = true;
this.candidate = null; this.request = null;
if (globalThis.Plotly?.purge) globalThis.Plotly.purge(this.plot);
this.plot.hidden = true;
this.viewer.clear(); this.title.textContent = 'Compensatory-mutation visualization';
}

show(candidate, request) {
this.candidate = candidate; this.request = request; this.controls.hidden = false;
this.showMode(candidate.interactions.mm ? 'mm' : Object.keys(MODE_LABELS).find((mode) => candidate.interactions[mode]) ?? 'mm');
}

showMode(mode) {
for (const button of this.controls.querySelectorAll('button')) button.setAttribute('aria-pressed', String(button.value === mode));
this.title.textContent = `${this.candidate.values.mutation}: ${MODE_LABELS[mode]}`;
const interaction = this.candidate.interactions[mode];
this.candidate = candidate; this.request = request;
this.title.textContent = candidate.values.mutation;
const interaction = candidate.interactions.ww;
if (!interaction) {
this.viewer.clear();
this.viewer.status.textContent = `${MODE_LABELS[mode]} has no favorable reported interaction.`;
return;
this.viewer.status.textContent = 'The wild type has no favorable reported interaction to visualize.';
} else {
const visualRequest = {
...request,
query: { ...request.query, records: [{ ...request.query.records[0], sequence: candidate.queryWildtype }] },
target: { ...request.target, records: [{ ...request.target.records[0], sequence: candidate.targetWildtype }] },
};
try {
this.viewer.show(interaction, visualRequest, { mutations: mutationAnnotations(candidate) });
if (!this.viewer.frame.hidden) this.viewer.status.textContent += ' Designed point mutations are annotated in dark green.';
} catch (error) { this.viewer.clear(); this.viewer.status.textContent = error.message; }
}
const querySequence = mode[0] === 'w' ? this.candidate.queryWildtype : this.candidate.queryMutated;
const targetSequence = mode[1] === 'w' ? this.candidate.targetWildtype : this.candidate.targetMutated;
const visualRequest = {
...this.request,
query: { ...this.request.query, records: [{ ...this.request.query.records[0], sequence: querySequence }] },
target: { ...this.request.target, records: [{ ...this.request.target.records[0], sequence: targetSequence }] },
};
this.viewer.show(interaction, visualRequest);
const plot = mutationEnergyPlot(candidate);
this.plot.hidden = false;
this.plot.setAttribute('aria-label', `Interaction energies for ${candidate.values.mutation}: ${ENERGY_STATES.map((state, index) => `${state} ${plot.data[0].y[index] ?? 'not available'}`).join(', ')} kcal/mol`);
if (globalThis.Plotly?.newPlot) globalThis.Plotly.newPlot(this.plot, plot.data, plot.layout, plot.config);
}
}

function numeric(value) {
return typeof value === 'number' && Number.isFinite(value) ? value : null;
}

export class CopomusResultsView {
constructor(root) {
this.root = root; this.candidates = []; this.key = 'rank'; this.direction = 1;
Expand Down
15 changes: 9 additions & 6 deletions gui/docs/copomus.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,15 @@ folding runs; larger screens remain available through `python/CopomuS.py`.
## Results and reproducibility

The sortable/filterable table contains the complete extended CopomuS TSV. Select
**View** to switch the embedded vaRRI visualization between the four mutation
states; a missing favorable interaction is reported explicitly. Session ZIPs
contain `parameter.json` and `copomus-mutations.tsv`. The JSON keeps native
IntaRNA settings and records the task and CopomuS settings under `gui`, so Load
restores the complete editable run. As with other GUI sessions, all data stays
in the tab and disappears on reload unless downloaded.
**View** to show the wild-type interaction once in vaRRI, with both designed
substitutions annotated at their original signed sequence coordinates in dark
green. A compact Plotly bar chart beside it compares the `ww`, `wm`, `mw` and
`mm` interaction energies and marks `ww` with a dotted baseline; unavailable
interactions remain empty. Session ZIPs contain `parameter.json` and
`copomus-mutations.tsv`. The JSON keeps native IntaRNA settings and records the
task and CopomuS settings under `gui`, so Load restores the complete editable
run. As with other GUI sessions, all data stays in the tab and disappears on
reload unless downloaded.

The WASM workflow vendors no generated runtime files in Git. Build staging
downloads Pyodide 314.0.7 from its npm release, verifies the pinned SHA-256,
Expand Down
Loading
Loading