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
31 changes: 31 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@

## Interface and handling

- --outNumberPerSeed selects each seed's best candidate sites before final
deduplication, energy/overlap/count filtering; model P ranks left-most-anchor
ensembles; zero preserves ordinary prediction; the GUI exposes the quota next
to the global output count (#285, #288 review)

- refine GUI seed and CSV control widths, filter examples by task, cap result
table height and use dark active tabs for additional output views (PR #303 review)

Expand Down Expand Up @@ -295,6 +300,32 @@ energies, restricted partition sums, and trackers.
################################################################################
################################################################################

261021 Alexander Mitrofanov
* OutputConstraint, OutputHandlerSeedCandidates, PredictorMfe, seeded predictors
+ Add bounded per-seed candidate selection before global pruning, preserving
eligible energy/traceback pairs and deduplicating sites before final filters.
* Reuse each MFE mode with an admitted single-seed adapter; preserve ordinary
statistics/trackers in one separate pass. Positive quotas can cost one
prediction per seed; zero retains the existing path.
* SeededPartitionFunction, PredictorMfeEns2d[Heuristic]SeedExtension
+ Rank/report left-most-anchor site ensemble energies independently of full
site partitions, Zall and actual-pair probabilities, per #288 guidance.
* CommandLineParsing, IntaRNA, README.md, doc/analysis/output-per-seed.md
+ --outNumberPerSeed=[0,1000], default zero; merge seed identities across
regions/windows and reset per region combination with --outPerRegion.
* Reject positive unseeded quotas, ignore evaluation, and apply final overlap
constraints to the retained union without refilling quotas (#285).
* gui/{options,protocol}.js, gui/tests/protocol.test.mjs,
scripts/wasm/{smoke-protocol.mjs,test-browser.py}
+ Expose outNumberPerSeed in the first GUI output row between outNumber and
outOverlap; move energy/accessibility filters to a dedicated second row.
+ Match native quota validation, window/region compatibility and JSON replay;
cover the default, range, rendered order and real WASM execution.
* PredictorSeedOracle_test.cpp, OutputHandlerSeedCandidates_test.cpp,
runOutputPerSeed.sh, library/test Makefile.am
+ Independent chain-enumeration, traceback, seed-only, mode-matrix, merge,
filter, CLI and unchanged-probability regressions; register/install sources.

261026 Alexander Mitrofanov
* gui/app.js, gui/{index.html,styles.css}, scripts/wasm/test-browser.py
* place Explicit seeds beside a half-width Seed constraint selector and halve
Expand Down
26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1781,6 +1781,30 @@ only one prediction is reported. Heuristic predictors additionally prune
candidate extensions during computation and can therefore consider fewer
interaction sites.

`--outNumberPerSeed=K` first retains each seed's best `K` distinct sites, then
merges their union and applies `--outNumber`, `--outOverlap`, `--outMaxE` and
`--outDeltaE`. The default `0` preserves the existing prediction path; positive
values range from 1 to 1000. With `1`, only the best candidate generated by each
seed can enter final output. Equal site boundaries are reported once, using the
lowest-energy eligible conformation. Final filtering does not refill a seed's
quota, so fewer than `outNumber` results can remain.

MFE models rank conformations constrained to contain the generating seed. Model
P ranks and reports the ensemble energy of conformations assigned to that seed
as their left-most anchor, using the same canonical seed ownership as its
partition computation. This can differ from the full site's ensemble energy.
Ensemble totals, prediction trackers and base-pair probabilities retain their
ordinary, unlimited-seed meaning. Heuristic modes keep their own recurrence
and extension restrictions. MFE modes run a separate prediction per admitted
seed, so enabling this option can substantially increase runtime; retained
candidate storage is bounded by `K` sites per seed.

Quotas apply across merged regions and overlapping windows of one sequence pair.
`--outPerRegion=true` resets them for each region combination, with windows of
that combination sharing a quota. Final overlap filtering of this candidate
union supports all overlap modes across regions/windows. A positive quota
requires seeds (`--noSeed` is incompatible); evaluation ignores this option.

Furthermore, it is possible to *restrict (sub)optimal enumeration* using

- `--outMaxE` : maximal energy for any interaction reported
Expand All @@ -1799,7 +1823,7 @@ Furthermore, it is possible to *restrict (sub)optimal enumeration* using
Overlap refers to the entire interval between the outermost intermolecular
base pairs, including unpaired positions inside that interval.

*Note*: non-overlapping output is heuristic, even with exact prediction
*Note*: with `--outNumberPerSeed=0`, non-overlapping output is heuristic, even with exact prediction
(`--mode=M`), and increases runtime. For each left interaction boundary, only
the best right extension is retained for overlap selection. If that extension
overlaps an earlier result, a shorter compatible extension may already have
Expand Down
86 changes: 86 additions & 0 deletions doc/analysis/output-per-seed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Per-seed output selection (#285, review #288)

The [maintainer's answers](https://github.com/BackofenLab/IntaRNA/pull/288#issuecomment-6319284289)
resolve the planning checkpoint in PR #288:

- Rank each seed's own candidates before forwarding them to overall selection.
- If different seeds select different conformations at identical site boundaries,
retain the lowest-energy eligible conformation and report the site once.
- For model P, use the ensemble assigned to the current seed as its left-most
anchor. The first-seed ownership contribution is the requested objective.
- Apply quotas across merged regions/windows, or independently per region
combination with `--outPerRegion`. Deduplicate, then apply final restrictions
without refilling quotas. Reject positive quotas with `--noSeed`; ignore them
for evaluation.

## Implementation

`OutputConstraint::reportMaxPerSeed` is a trailing constructor argument defaulting
to zero. CLI `--outNumberPerSeed` accepts integers in [0,1000]. Zero follows the
existing ranking, traceback, ensemble and output path with no candidate storage.

`OutputHandlerSeedCandidates` keeps at most K distinct sites for each generating
seed, keyed by its start pair in original sequence coordinates. Rediscovering a
site in an overlapping window spends no additional quota. The selected union is
deduplicated by its outer pairs; energy and conformation are retained together.
Deterministic energy/site ordering precedes final energy-window, overlap and
count filtering. No rejected candidate is replaced by the seed's next choice.
The CLI owns the collector across calls; direct predictor API users receive the
same selection within each `predict()` call. Independent regions reset quotas
only after all their windows have completed.

MFE prediction reuses each selected mode's own implementation with a non-owning
single-seed adapter and an unrestricted intermediate output constraint. This
preserves seed-constrained conformations, traceback and the mode's heuristic
recurrences, including S/H, S/M, X/H, X/M, X/R, X/K, B/H and seed-only modes.
The adapter reuses the admitted seed table and forwards full seed annotations.
Enabling the option costs one prediction per admitted seed. When statistics or
trackers are requested, a separate ordinary prediction supplies them exactly
once; seed-specific passes never contribute. This favors explicit, verifiable
semantics over adding seed-indexed DP state to every predictor family. Retained
candidate storage is O(K times the number of seed starts), excluding the reused
prediction workspace and retained traceback lengths. Custom MFE predictor
subclasses can override `predictPerSeed` to supply a factory preserving their
recurrence/state; incompatible inherited factories raise an explicit error when
a positive quota is requested. Complete-site spans are validated before each
seed-specific rank update, including legacy DP cells reserved for extensions.
Runtime can increase
substantially on long sequences with many seeds.

Model P consumes the existing anchor decomposition directly. The stack-seed
kernel's optional `anchorComplete` callback supplies each canonical left-most
anchor's whole hybrid partition at each complete site before full-site
aggregation. It sums noLP support states before publishing the candidate.
Legacy bulged-seed extension uses its existing anchor-specific left/right
contributions, retaining that mode's approximation and numerical limitations.
Model P reports the selected anchor ensemble's energy, including the site's
ordinary boundary/accessibility terms; it does not substitute the full site's
energy. Ensemble reports continue to represent site boundaries and annotate
the generating seed. The stack backend's canonicalization of nested explicit
seeds is unchanged: a containing seed is not an additional owner.

`Zall`, tracker callbacks and base-pair probability matrices remain the ordinary
prediction's quantities. Neither per-seed quotas nor final overlap/count/energy
selection changes the forward/outside objective. Exact/heuristic partition flags
are forwarded with those statistics. Probability SVG structure rendering keeps
its existing site-MFE representation.

## Validation

Regression coverage includes independent exhaustive chain enumeration grouped
by each contained seed for exact MFE models and by the left-most seed for exact
ensembles; quotas 1, 2 and effectively unlimited; cropped/reversed coordinates;
noLP; qualifying traceback energy; seed-only prediction; duplicate sites and
windows; final overlap/energy/count selection without quota refills; and all
currently dispatched seeded CLI modes. CLI checks also cover explicit zero,
parameter files, invalid integers, unseeded/evaluation behavior, merged versus
independent regions, deterministic multithreaded windows and unchanged actual
base-pair probabilities.

GCC 14 / ViennaRNA 2.7.2 release and debug `make tests -j2` both passed all nine
suites (123 API cases; 529,850 release / 529,851 debug assertions). The installed
pkg-config consumer and standalone compilation of the five affected public
headers passed. Out-of-tree builds used the source `src/` include path, the
generated source-side `IntaRNA.pc` expected by current build rules, and copies of
the unchanged CLI fixtures in each build's `tests/data/`. No expected prediction
outputs were regenerated.
5 changes: 4 additions & 1 deletion gui/docs/step3-frontend-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ keep their scientific grouping without stretching across the complete panel,
use 16 px horizontal gaps and bottom-align labels above their controls.
The Seed tab places the constraint selector (12 rem) beside Explicit seeds on
desktop; the CSV sort selector uses a 19 rem track. Both controls stack at mobile
widths.
widths. Output places the global count, per-seed candidate count and overlap mode
in its first row; energy/accessibility thresholds form the second row and the
existing boolean filters follow in the third. Per-seed selection defaults to zero
and accepts the native 0–1,000 range.
The institutional link reads “Bioinformatics - University of Freiburg” and opens
the group website. Footer and table notes use the available width, wrapping only
when the viewport requires it.
Expand Down
8 changes: 7 additions & 1 deletion gui/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const fields = new Map([
number('windowWidth', 'Prediction window width', 0, 0, 99999, { ...prediction, hint: '0 disables prediction windows; otherwise at least 10 nt.' }),
number('windowOverlap', 'Prediction window overlap', 150, 0, 99999, prediction),
number('outNumber', 'Maximum interactions per sequence pair', 1, 0, 100, { ...prediction, hint: 'Browser limit: 100; 0 computes accessibility only.' }),
number('outNumberPerSeed', 'Best sites retained per seed', 0, 0, 1000, { ...prediction, hint: '0 disables per-seed candidate selection; positive values can increase runtime.' }),
select('outOverlap', 'Allow overlapping interactions', 'B', { B: 'Both strands', N: 'Neither strand', T: 'Target only', Q: 'Query only' }, prediction),
number('outMaxE', 'Maximum interaction energy (exclusive, kcal/mol)', 0, -999, 999, { ...prediction, step: 'any' }),
number('outDeltaE', 'Maximum difference from MFE (kcal/mol)', 100, 0, 100, { ...prediction, step: 'any' }),
Expand Down Expand Up @@ -86,7 +87,12 @@ export const GROUPS = [
['noSeed'], ['seedTQ'], ['seedBP', 'seedMaxUP', 'seedQMaxUP', 'seedTMaxUP'],
['seedMaxE', 'seedMinPu', 'seedMaxEhybrid'], ['seedQRange', 'seedTRange'], ['seedNoGU', 'seedNoGUend'],
]) },
{ key: 'output-options', label: 'Output', fields: [...fields.values()].filter((field) => field.key.startsWith('out') && field.key !== 'outCsvSort') },
{ key: 'output-options', label: 'Output', rows: rows([
['outNumber', 'outNumberPerSeed', 'outOverlap'],
['outMaxE', 'outDeltaE', 'outMinPu'],
['outBestSeedOnly', 'outNoLP', 'outNoGUend', 'outPerRegion', 'outPairwise'],
['outSpotProbSpots'],
]) },
{ key: 'csv-options', label: 'CSV', fields: [fields.get('outCsvSort')] },
{ key: 'interaction-options', label: 'Model & Energy', rows: rows([
['model', 'mode'], ['energyVRNA', 'temperature', 'energyAdd'], ['energyNoDangles'],
Expand Down
5 changes: 3 additions & 2 deletions gui/protocol.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ export function prepareRequest(request) {
} else {
if (parameters.model === 'B' && parameters.mode !== 'H') throw new Error('Helix-block model B supports heuristic mode only.');
if (parameters.noSeed && ['S', 'K'].includes(parameters.mode)) throw new Error('Seed-only and greedy prediction require a seed.');
if (parameters.noSeed && parameters.outNumberPerSeed > 0) throw new Error('Per-seed candidate selection requires a seed.');
if (parameters.mode === 'K' && (parameters.model !== 'X' || needsEnsemble)) throw new Error('Greedy prediction requires model X and cannot produce ensemble/probability output.');
if (parameters.seedTQ) validateInteractions(parameters.seedTQ, oneRecord(qRecords, 'Explicit seeds'), oneRecord(tRecords, 'Explicit seeds'), parameters.qIdxPos0, parameters.tIdxPos0, ',');
const minimumSeedLength = parameters.seedTQ ? Math.min(...parameters.seedTQ.split(',').map((seed) => seed.split('&')[0].split('|').length - 1)) : parameters.seedBP;
Expand All @@ -196,11 +197,11 @@ export function prepareRequest(request) {
if (outputs.some((key) => ['bpProb', 'bpsvg'].includes(key)) && (parameters.model !== 'P' || !['H', 'M'].includes(parameters.mode) || parameters.windowWidth || parameters.mode === 'M' && !parameters.noSeed && (parameters.seedTQ || parameters.seedMaxUP || parameters.seedQMaxUP > 0 || parameters.seedTMaxUP > 0))) throw new Error('Base-pair probabilities require model P, heuristic/exact mode and no windows; exact mode supports no seed or stack-only seeds.');
if (parameters.windowWidth) {
const maxLength = Math.max(...['q', 't'].map((p) => parameters[`${p}Acc`] === 'N' ? parameters[`${p}IntLenMax`] : Math.max(parameters[`${p}AccW`], parameters[`${p}IntLenMax`])));
if (parameters.windowWidth < 10 || parameters.windowOverlap >= parameters.windowWidth || parameters.windowOverlap < maxLength || ['q', 't'].some((p) => !parameters[`${p}IntLenMax`] && (parameters[`${p}Acc`] === 'N' || !parameters[`${p}AccW`])) || parameters.outNumber > 1 && parameters.outOverlap !== 'B' || needsEnsemble || parameters.model === 'P') throw new Error('Prediction windows require width ≥ 10, overlap below width and at least the bounded interaction lengths, overlapping output, and no ensemble statistics/model P.');
if (parameters.windowWidth < 10 || parameters.windowOverlap >= parameters.windowWidth || parameters.windowOverlap < maxLength || ['q', 't'].some((p) => !parameters[`${p}IntLenMax`] && (parameters[`${p}Acc`] === 'N' || !parameters[`${p}AccW`])) || parameters.outNumberPerSeed === 0 && parameters.outNumber > 1 && parameters.outOverlap !== 'B' || needsEnsemble || parameters.model === 'P') throw new Error('Prediction windows require width ≥ 10, overlap below width and at least the bounded interaction lengths, overlapping output unless per-seed selection is enabled, and no ensemble statistics/model P.');
}
// Non-overlap constraints in native merged regions require a single region on
// the relevant strand; automatic region detection is only safe per region.
if (!parameters.outPerRegion && parameters.outOverlap !== 'B') {
if (!parameters.outPerRegion && parameters.outNumberPerSeed === 0 && parameters.outOverlap !== 'B') {
for (const p of ['q', 't']) {
if ((parameters.outOverlap === 'N' || parameters.outOverlap === (p === 'q' ? 'T' : 'Q')) && (parameters[`${p}Region`].includes(',') || parameters[`${p}RegionLenMax`])) throw new Error('Non-overlapping output with multiple/automatic regions requires output per region.');
}
Expand Down
Loading
Loading