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

## Interface and handling

- deterministic kinetic seed extension with --model=X --mode=K and configurable
local scoring; complete-energy downhill steps, atomic noLP loop/stack moves
and retained greedy traceback; equilibrium probability output is unsupported
- kinetic mode always uses stacked extensions and trusts handler-provided seeds;
reuse candidate pairing/local energies
- IntaRNAsnap personality enables kinetic seed extension with noLP by default

- apply outDeltaE relative to the sequence pair's best interaction when merging
regions; preserve local windows for outPerRegion output (PR #253)

Expand All @@ -35,6 +42,8 @@

## Technical changes and Optimizations

- install personality links in out-of-tree builds, including IntaRNAsnap

- BUGFIX : normalize single-pair suboptimal boundaries before traceback and
boundary-only output validation (PR #253)

Expand Down Expand Up @@ -76,6 +85,26 @@
- BUGFIX : multi-threading : IntaRNAsTar was not thread-safe due to shared
storage of computed ranges

# IntaRNAsnap

New personality IntaRNAsnap identifies the fastest "folding path" to a (locally)
optimal interaction after seed formation, rather than finding the global optimum
under the assumption of thermodynamic equilibrium.

To this end, IntaRNAsnap implements a kinetics-motivated greedy search for the
best interaction that can be reached from the given seed(s) by a series of
complete-energy downhill local moves.
Local moves are direct stack extensions or 2-base-pair loop/stack moves, i.e.
the same moves as used in the IntaRNA noLP model. That way, kinetic energy
barriers posed by the formation of loops and bulges are avoided, which mimics
the continuation of the zipping process after "jumping" over a loop or bulge.

This personality is only available with --model=X and --mode=K, and equilibrium
probability output is unsupported. The local scoring can be configured with
--kineticScore=A|B|C, and the output can include distinct structures, energies,
restricted partition sums, and trackers.


# IntaRNAeval

New personality of IntaRNA to evaluate predefined RNA-RNA interactions.
Expand All @@ -97,6 +126,38 @@ energies, restricted partition sums, and trackers.
################################################################################
################################################################################

261005 Alexander Mitrofanov
* bin/CommandLineParsing, tests/runKineticSeedExtension.sh :
+ add IntaRNAsnap personality with model=X, mode=K and outNoLP=true defaults;
support executable-name and explicit personality selection
* configure.ac :
* find personality declarations via srcdir so out-of-tree installations
create the executable links, including IntaRNAsnap
* README.md, doc/kinetic-seed-extension.md,
doc/recursions/IntaRNAsnap.PredictorSeedExtensionKinetic.svg, doc/Makefile.am :
+ document personality usage and depict seed initialization, allowed moves,
complete-energy greedy selection, stopping and prefix reporting
+ distribute the benchmark's tutorial sequences with its script and results
* doc/benchmark-kix.py, doc/kix-benchmark-20261005.json :
+ compare IntaRNAsnap with default IntaRNA with and without GU-end constraints;
record wall time, peak RSS, MFE energy and maximum covered strand length
* implement https://github.com/BackofenLab/IntaRNA/pull/254#issuecomment-5995041057

261005 Alexander Mitrofanov
* IntaRNA/PredictorSeedExtensionKinetic :
* always evaluate single and double stacks plus atomic loop/stack extensions;
trust seed-handler structures/energies, including explicit lonely pairs
* cache each end's candidates, shared complementarity checks and local
energies; rebuild only the chosen end and refresh full opposite-end energy
* bin/CommandLineParsing :
* set --outNoLP=true with INFO for kinetic mode K
* tests/PredictorSeedExtensionKinetic_test.cpp, tests/runKineticSeedExtension.sh :
* update independent oracle and CLI expectations for atomic double stacks;
cover trusted seeds, caching and kinetic mode K
* README.md, doc/kinetic-seed-extension.md :
* document revised semantics in response to
https://github.com/BackofenLab/IntaRNA/pull/254

261005 Alexander Mitrofanov
* IntaRNA/PredictorMfe, tests/PredictorMfeHeuristicCellState_test.cpp,
tests/PredictorMfeEnsRegression_test.cpp :
Expand Down Expand Up @@ -162,6 +223,23 @@ energies, restricted partition sums, and trackers.
+ define suboptimal predictions by distinct start/end coordinates on both RNAs
* distinguish site prediction from evaluation of supplied structures (PR #253)

261003 Alexander Mitrofanov
* IntaRNA/PredictorSeedExtensionKinetic, src/IntaRNA/Makefile.am :
+ greedily extend seeds on either side using complete interaction-energy
differences and deterministic thermodynamic or distance-weighted scores
+ support noLP macro-steps, GU restrictions, per-strand loop/span constraints,
explicit-seed validation and cached trajectory traceback
+ retain valid visited prefixes and select non-overlapping output from the
complete retained candidate set; reject unsupported ensemble statistics
* bin/CommandLineParsing :
+ expose --mode=K exclusively for --model=X and --kineticScore=A|B|C
* tests/PredictorSeedExtensionKinetic_test.cpp, tests/runKineticSeedExtension.sh,
tests/Makefile.am :
+ validate local move choices, macro-step energetics, structural constraints,
traceback, output filtering and CLI compatibility
* README.md, doc/kinetic-seed-extension.md, doc/Makefile.am :
+ document scientific scope and complete-energy move enumeration

261002 Alexander Mitrofanov
* doc/analysis/out-overlap.md, doc/analysis/out-overlap/reproduce.py :
+ analyse all four output overlap modes and the documented enumeration limits
Expand Down
61 changes: 61 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ The following topics are covered by this documentation:
- [IntaRNAsTar - optimized for sRNA-target prediction](#IntaRNAsTar)
- [IntaRNAseed - identifys and reports seed interactions only](#IntaRNAseed)
- [IntaRNAens - ensemble-based prediction and partition function computation](#IntaRNAens)
- [IntaRNAsnap - kinetic seed extension](#IntaRNAsnap)
- [IntaRNAeval - evaluate predefined interactions](#IntaRNAeval)
- [How to constrain predicted interactions](#constraintSetup)
- [Interaction restrictions](#interConstr)
Expand Down Expand Up @@ -729,6 +730,40 @@ minimum free energy interaction.
Putative seed interactions (used by the `H` and `M` mode) can be enumerated
and studied using the `S` mode.

### Greedy kinetic seed extension

`--model=X --mode=K` grows each available seed along a deterministic greedy
path. Every step compares feasible extensions on both sides using the complete
change in interaction energy, including accessibility, terminal penalties and
dangling ends. Only strictly negative changes are accepted. Extensions always
add one stacked pair or two stacked pairs, including across a loop; two-pair
moves are evaluated and committed atomically. The CLI sets `--outNoLP=true`
when absent or false and logs an INFO message. Seeds and their energies are
accepted from the seed handler, including explicit seeds with lonely pairs.

`--kineticScore` selects the local move ranking:

| Value | Score minimized for a move with gaps `s1`, `s2` |
| --- | --- |
| `A` (default) | Complete energy change |
| `B` | Complete energy change / `(1+s1+s2)` |
| `C` (C1 in the design) | Complete energy change / `(1+2*max(s1,s2))` |

Equal scores prefer the left side, then fewer unpaired bases, then smaller
`s1`, then the single-pair move. The denominators also apply to two-pair moves. All reportable visited
states, including seeds, participate in the normal energy-ranked output;
traceback preserves the actual chosen path. `--outNoGUend`, separate query and
target loop/span limits, regions, output energy/accessibility filters and
overlap settings remain applicable.

The [IntaRNAsnap personality](#IntaRNAsnap) selects this mode with noLP enabled
by default. It is a zippering-inspired heuristic without a calibrated time axis
or a guarantee of the global minimum. Equilibrium probability/partition-sum
outputs are rejected, as are other models and `--noSeed`. Scores B and C are
optional distance preferences, not measured kinetic rates. See the
[algorithm and preliminary benchmark](doc/kinetic-seed-extension.md).



[![up](doc/figures/icon-up.28.png) back to overview](#overview)
<br /><br />
Expand Down Expand Up @@ -1003,6 +1038,32 @@ IntaRNA --mode=S ...
[![up](doc/figures/icon-up.28.png) back to overview](#overview)


### IntaRNAsnap

**IntaRNAsnap** (kinetic seed extension) grows every handler-provided seed by
choosing the most favorable complete energy change at either end. It sets
`--model=X --mode=K --outNoLP=true`; other defaults are those of IntaRNA.
Each move adds one stacked pair, two stacked pairs, or a loop-closing pair
plus its outward stack. Two-pair moves are evaluated and committed together.
Seeds themselves may contain lonely pairs when supplied by the seed handler.

The following calls are equivalent:

```sh
IntaRNAsnap -t target.fasta -q query.fasta
IntaRNA --personality=IntaRNAsnap -t target.fasta -q query.fasta
IntaRNA --model=X --mode=K --outNoLP=true -t target.fasta -q query.fasta
```

Only strictly downhill moves are accepted. The default score A chooses the
largest energy decrease; `--kineticScore=B|C` adds distance preferences.
The reported MFE is the best visited, reportable interaction across seeds;
this heuristic greedy search has no global-optimum or physical folding-time guarantee.


[![up](doc/figures/icon-up.28.png) back to overview](#overview)


### IntaRNAeval

**IntaRNAeval** evaluates predefined RNA-RNA interactions with the selected
Expand Down
2 changes: 1 addition & 1 deletion configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,7 @@ AS_IF([test "$DEPENDENCYNOTFOUND" = "1"], [
##########################################################################

# get available personalities
AC_SUBST([PERSONALITIES],[`grep 'return.*"IntaRNA..*"' ./src/bin/CommandLineParsing.h | grep -o '".*"' | tr -d '"' | tr "\n" " "`])
AC_SUBST([PERSONALITIES],[`grep 'return.*"IntaRNA..*"' "$srcdir/src/bin/CommandLineParsing.h" | grep -o '".*"' | tr -d '"' | tr "\n" " "`])
##########################################################################

# Keep strict diagnostics focused on IntaRNA sources. Boost and ViennaRNA
Expand Down
3 changes: 0 additions & 3 deletions doc/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
################################################################

EXTRA_DIST = \
analysis/out-overlap.md \
analysis/out-overlap/reproduce.py \
conda.txt \
doxygen.cfg \
latex-deps/adjcalc.sty \
Expand All @@ -15,4 +13,3 @@ EXTRA_DIST = \
latex-deps/tocloft.sty \
latex-deps/trimclip.sty \
latex-deps/xtab.sty

149 changes: 149 additions & 0 deletions doc/benchmark-kix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
#!/usr/bin/env python3
"""Small, single-thread comparison of IntaRNAsnap and default IntaRNA.

Requires Python 3, GNU time and a release IntaRNA binary. Run from any directory:
python3 doc/benchmark-kix.py /path/to/IntaRNA --output results.json
"""

import argparse
import csv
import hashlib
import io
import json
import os
from pathlib import Path
import platform
import statistics
import subprocess
import sys
import tempfile
import time


PAIRS = [("fhlA", "OxyS"), ("phoB", "GcvB"), ("ilvE", "GcvB.ST")]
COLUMNS = "start1,end1,start2,end2,E,hybridDB"


def read_fasta(path):
lines = path.read_text().splitlines()
assert sum(line.startswith(">") for line in lines) == 1, path
sequence = "".join(line.strip() for line in lines if not line.startswith(">"))
return {"file": "handson/" + path.name, "header": lines[0][1:],
"length_nt": len(sequence), "sequence": sequence,
"sha256": hashlib.sha256(path.read_bytes()).hexdigest()}


def prediction(stdout):
rows = list(csv.DictReader(io.StringIO(stdout), delimiter=";"))
if not rows:
return None
assert len(rows) == 1, rows
row = rows[0]
result = {key: int(row[key]) for key in ("start1", "end1", "start2", "end2")}
result.update(E_kcal_mol=float(row["E"]), hybridDB=row["hybridDB"])
# All inputs use the default one-based, ascending coordinates.
result["length_nt"] = max(result["end1"] - result["start1"] + 1,
result["end2"] - result["start2"] + 1)
return result


def distribution(samples):
return {"median": statistics.median(samples), "min": min(samples), "max": max(samples)}


def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("binary", type=Path)
parser.add_argument("--output", required=True, type=Path)
parser.add_argument("--repetitions", type=int, default=5)
parser.add_argument("--warmups", type=int, default=1)
parser.add_argument("--time", type=Path, default=Path("/usr/bin/time"))
parser.add_argument("--build-description", default="unspecified")
args = parser.parse_args()
if args.repetitions < 1 or args.warmups < 0:
parser.error("repetitions must be positive and warmups nonnegative")
binary = args.binary.resolve()
timer = args.time.resolve()
fixtures = Path(__file__).resolve().parent / "handson"
common = ["--threads=1", "--outMode=C", "--outCsvCols=" + COLUMNS,
"--outNumber=1", "--default-log-file=/dev/null"]
env = dict(os.environ, OMP_NUM_THREADS="1")
report = {
"schema": 1,
"binary_sha256": hashlib.sha256(binary.read_bytes()).hexdigest(),
"version": subprocess.check_output([str(binary), "--version"], text=True).strip(),
"build": args.build_description,
"platform": platform.platform(), "machine": platform.machine(),
"python": platform.python_version(),
"timer": subprocess.check_output([str(timer), "--version"], text=True).splitlines()[0],
"common_arguments": common,
"repetitions": args.repetitions, "warmups_per_configuration": args.warmups,
"timing": "perf_counter wall seconds around GNU time + binary; includes startup, folding, seed search and prediction",
"memory": "GNU time %M: maximum resident set size of each child process, KiB",
"noGU": "--outNoGUend=true; seedNoGU remains false for both programs",
"baseline": "default IntaRNA (model X, mode H, outNoLP false)",
"comparison": "IntaRNAsnap defaults (model X, mode K, outNoLP true, kineticScore A)",
"length": "max(end1-start1+1, end2-start2+1), nt; default one-based coordinates",
"deviations": "signed IntaRNAsnap minus default IntaRNA, within each GU setting; not a global-optimum error bound",
"cases": [],
}
with tempfile.TemporaryDirectory(prefix="intarna-kix-benchmark-") as directory:
rss_file = Path(directory) / "time.txt"
for target, query in PAIRS:
inputs = ["--target=" + str(fixtures / (target + ".fasta")),
"--query=" + str(fixtures / (query + ".fasta"))]
case = {"name": target + "/" + query,
"target": read_fasta(fixtures / (target + ".fasta")),
"query": read_fasta(fixtures / (query + ".fasta")), "runs": [], "comparisons": []}
configurations = []
for no_gu in (False, True):
for program in ("IntaRNA", "IntaRNAsnap"):
options = [] if program == "IntaRNA" else ["--personality=IntaRNAsnap"]
options.append("--outNoGUend=" + str(no_gu).lower())
configurations.append((program, no_gu, options))
case["runs"].append({"program": program, "noGU": no_gu,
"arguments": options, "samples": []})
# Warm each configuration; rotate execution order every round.
for iteration in range(args.warmups + args.repetitions):
for offset in range(len(configurations)):
index = (iteration + offset) % len(configurations)
program, no_gu, options = configurations[index]
run = case["runs"][index]
command = [str(binary), *common, *inputs, *options]
start = time.perf_counter()
process = subprocess.run([str(timer), "-f", "%M", "-o", str(rss_file),
*command], env=env, text=True, capture_output=True, check=True)
elapsed = time.perf_counter() - start
result = prediction(process.stdout)
if "prediction" in run:
assert run["prediction"] == result, (case["name"], options, process.stdout)
run["prediction"] = result
if iteration >= args.warmups:
run["samples"].append({"wall_s": elapsed, "peak_rss_kib": int(rss_file.read_text())})
for run in case["runs"]:
run["wall_s"] = distribution([sample["wall_s"] for sample in run["samples"]])
run["peak_rss_kib"] = distribution([sample["peak_rss_kib"] for sample in run["samples"]])
# Outside timing, reevaluate each reported structure independently.
if run["prediction"] is not None:
evaluated = subprocess.check_output([str(binary), *common, *inputs,
"--rri=" + run["prediction"]["hybridDB"]],
env=env, text=True)
assert prediction(evaluated) == run["prediction"], (case["name"], run)
run["structure_reevaluation_matches"] = True
for index, no_gu in ((0, False), (2, True)):
baseline, kix = case["runs"][index:index + 2]
available = baseline["prediction"] is not None and kix["prediction"] is not None
case["comparisons"].append({
"noGU": no_gu,
"delta_E_kcal_mol": round(kix["prediction"]["E_kcal_mol"] - baseline["prediction"]["E_kcal_mol"], 2) if available else None,
"delta_length_nt": kix["prediction"]["length_nt"] - baseline["prediction"]["length_nt"] if available else None,
"wall_ratio_kix_over_default": kix["wall_s"]["median"] / baseline["wall_s"]["median"],
"rss_ratio_kix_over_default": kix["peak_rss_kib"]["median"] / baseline["peak_rss_kib"]["median"],
})
report["cases"].append(case)
print(case["name"] + ": " + json.dumps(case["comparisons"]), file=sys.stderr, flush=True)
args.output.write_text(json.dumps(report, indent=2) + "\n")


if __name__ == "__main__":
main()
Loading
Loading