RNA Melt — Help

How to run analyses in the browser, on the CLI, and from Python.

← back to app

1. Using the tool in the browser

The browser interface runs the full analysis pipeline locally via Pyodide (CPython compiled to WebAssembly). Your data never leaves your machine — there is no server, no upload.

CSV format

Column 0 is temperature in °C (any header name). Columns 1+ are signal columns — absorbance or fluorescence. Rows where temperature is missing or all values are NaN are dropped.

temperature, sample_1, sample_2, sample_3
20.0,        0.412,    0.388,    0.401
20.5,        0.415,    0.391,    0.404
...

Workflow

  1. Drag & drop a CSV onto the upload zone — or click to browse.
  2. Pick a signal column from the dropdown, or switch to __multi__ / __concentration__ mode for multi-column analyses.
  3. Use the dual-range slider to set the transition window [T_low, T_high] (°C).
  4. Adjust the baseline offsets. Folded baseline is fit on [T_low, T_low + lower offset], unfolded on [T_high − upper offset, T_high].
  5. Set strand concentration (µM) and salt (mM).
  6. Click Run Analysis. Three Tm estimates are returned: raw (baseline intersection), van't Hoff linearisation, and the full nonlinear two-state fit.

Three analysis modes

ModeColumn dropdownWhat it does
Single column any signal column Tm by baseline intersection, van't Hoff linearisation, and a full two-state nonlinear fit. Returns ΔH / ΔS / ΔG / Tm from each method.
Shared-ΔH multi __multi__ Joint fit across all columns sharing ΔH and ΔS, with independent baselines per column. Each column carries its own concentration.
Concentration series __concentration__ Extracts three Tm values per column (raw / vH / full fit) and runs 1/Tm = (R/ΔH)·ln(C_T/f) + ΔS/ΔH for each. f = 1 for homodimers, f = 4 for heterodimers.

Chart interactions

All three plots (melting curve, van't Hoff / concentration regression, derivative) are interactive:

ActionGesture
Zoom in / outMouse wheel scroll over the chart
PanClick and drag inside the chart area
Pinch zoomTwo-finger pinch on touch devices
Reset viewClick the ⤢ reset button in the chart header

Zooming and panning act on both axes simultaneously. The shaded analysis-window overlay on the melting-curve plot follows the zoom — useful for inspecting the baseline regions or the steep part of the transition without re-running the fit.

Download results

The Download CSV button saves a results file containing a per-column block (raw / vH / full-fit / multi columns) and, for concentration mode, an additional block with the per-curve table and the three regressions.

2. Command-line interface

The same pipeline runs outside the browser for batch / scripted use. The full result dict is printed as JSON on stdout. Pass --csv-out to also write a results CSV in the same format the browser produces.

Running

From the project root, with pandas, numpy, and scipy available:

python -m rnamelt FILE.csv [options]

Installing the package (pip install .) also wires up a console-script alias so you can just run rnamelt FILE.csv. python -m rnamelt --help lists every flag. Exit codes: 0 on success, 1 if the analysis returns an error, 2 on bad input.

Examples

Default — van't Hoff + full fit on every signal column in the file:

python -m rnamelt melt.csv --csv-out batch.csv

Single column:

python -m rnamelt melt.csv \
  --column sample_1 --struct-type heterodimer \
  --oligo 5.0 --T-low 20 --T-high 90 \
  --csv-out single.csv

Shared-ΔH fit across columns (defaults to all columns at --oligo when --oligo-multi is omitted):

python -m rnamelt melt.csv \
  --column __multi__ --struct-type heterodimer \
  --oligo-multi sample_1=0.5 \
  --oligo-multi sample_2=5.0 \
  --oligo-multi sample_3=50

Concentration-series van't Hoff (requires --oligo-multi — varying CT is the whole point):

python -m rnamelt melt.csv \
  --column __concentration__ --struct-type heterodimer \
  --oligo-multi sample_1=0.5 \
  --oligo-multi sample_2=5.0 \
  --oligo-multi sample_3=50 \
  --csv-out conc.csv

Flags

FlagDefaultNotes
csv (positional)CSV path
--column(omit → all columns)Run single-mode (vH + full fit) on every signal column when omitted. Pass a column name for one column, __multi__ for shared-ΔH joint fit, or __concentration__ for the 1/Tm vs ln(CT/f) series.
--struct-typeheterodimerheterodimer · homodimer · monomer
--signal-typeabsorbanceabsorbance · fluorescence
--T-low / --T-highdata rangeTransition window (°C)
--bl-lower / --bl-upper10Baseline offsets (°C)
--salt150NaCl in mM
--oligo0.5Single-column strand concentration (µM)
--oligo-multi NAME=VALRepeatable per-column µM for multi / concentration modes
--csv-outWrite a results CSV
--indent2JSON indent (0 = compact)

Three solver-tuning groups override the in-browser defaults (rnamelt.SOLVER_DEFAULTS, VH_DEFAULTS, FIT_INIT_DEFAULTS). Pass any subset; unset flags fall back to defaults.

GroupFlags
scipy.optimize.least_squares--max-nfev, --ftol, --gtol, --xtol, --method, --loss, --f-scale, --jac, --solver-verbose, --residuals-method
van't Hoff linearisation--vh-border, --vh-t1-min, --vh-t1-max, --vh-T-scale
full-fit initial guesses--dH-init, --dS-init, --lin-init

3. Python API

Install from source:

pip install .                # base — pandas / numpy / scipy
pip install '.[plots]'       # also pulls matplotlib for .plot() helpers

The recommended surface is the MeltAnalysis class. It holds the DataFrame plus every solver / van't Hoff / fit-init knob, and exposes the three modes as methods that return typed result objects from rnamelt.results. The thin functional wrappers (analyze_single, …) are kept for back-compat and also documented below.

MeltAnalysis class

from rnamelt import MeltAnalysis, FitFailed

m = MeltAnalysis.from_csv(
    "melt.csv",
    struct_type="heterodimer",   # "heterodimer" | "homodimer" | "monomer"
    salt=150.0,                  # NaCl (mM) — metadata only
    T_low=None, T_high=None,     # transition window (°C); None = full range
    bl_lower_offset=10.0,        # folded-baseline span above T_low (°C)
    bl_upper_offset=10.0,        # unfolded-baseline span below T_high (°C)
    solver=None,                 # dict — overrides rnamelt.SOLVER_DEFAULTS
    vh=None,                     # dict — overrides rnamelt.VH_DEFAULTS
    fit_init=None,               # dict — overrides rnamelt.FIT_INIT_DEFAULTS
)

print(m.signal_columns)          # ['F4', 'F5', ...]
m.configure(salt=1000.0)         # chainable in-place reconfiguration
m2 = m.copy()                    # independent fork

Single column — returns a SingleResult:

r = m.single("F4", oligo=0.5)    # oligo in µM
r.Tm_raw, r.vh.dH, r.fit.Tm, r.fit.dG
len(r.fit.curve)                 # numpy arrays live on the result

try:
    r.fit.require()              # raises FitFailed if not r.fit.ok
except FitFailed as e:
    print(e)

r.to_dict()       # legacy orchestrator dict (browser-shape)
r.to_dataframe()  # 1-row tidy summary
r.to_csv("out.csv")

Shared-ΔH multi fit — returns a MultiResult:

oligo_multi = {"F4": 0.5, "F5": 1.0, "F6": 2.0, "F7": 4.0}
multi = m.multi(oligo_multi)
multi.dH, multi.dS, multi.dG     # shared ΔH/ΔS/ΔG
for c in multi.columns:
    print(c.name, c.Tm_fit, c.oligoC)

Concentration-series van't Hoff — returns a ConcentrationResult with three regressions:

conc = m.concentration(oligo_multi)
for s in (conc.series_raw, conc.series_vh, conc.series_fit):
    if s.ok:
        print(s.dH, s.dG_37, s.r_squared)
conc.per_curve, conc.skipped

Every column at once:

batch = m.single_all(oligo=0.5)  # dict[str, SingleResult]
{name: r.fit.Tm for name, r in batch.items() if r.fit and r.fit.ok}

From raw arrays — no CSV

MeltAnalysis.from_arrays bypasses the CSV layer. signals accepts three shapes:

import numpy as np
from rnamelt import MeltAnalysis

T = np.linspace(20, 90, 71)

# (1) 1D array — single column, auto-named "signal_1"
m = MeltAnalysis.from_arrays(T, signal_F4)

# (2) Mapping {name: 1D array} — names preserved
m = MeltAnalysis.from_arrays(T, {"F4": sigF4, "F5": sigF5})

# (3) 2D array (rows=T, cols=signals), optional `names=`
m = MeltAnalysis.from_arrays(T, arr2d, names=["A", "B", "C"])

All signal arrays must match the temperature length. Other keyword arguments forward to MeltAnalysis.__init__.

Plotting (optional — needs matplotlib)

Each result class has a .plot() method that lazy-imports rnamelt.plots (matplotlib stays out of the base install so the browser bundle is unaffected).

import matplotlib
matplotlib.use("Agg")                    # headless backend
import matplotlib.pyplot as plt

# SingleResult — 3-panel: raw + baselines / van't Hoff / full fit
fig, axes = m.single("F4").plot(figsize=(15, 5))
fig.savefig("single.png", dpi=120); plt.close(fig)

# Convenience shortcut on the analyzer
fig, axes, result = m.plot("F4")         # single(column).plot()

# MultiResult — all column curves + shared-ΔH fits on one axis
fig, ax = m.multi(oligo_multi).plot(figsize=(10, 6))

# ConcentrationResult — overlay + 1/Tm vs ln(C_T/f)
fig, axes = m.concentration(oligo_multi).plot(figsize=(13, 5))

Functional helpers

Thin wrappers kept for backwards compatibility. Each takes a cleaned pandas.DataFrame and returns the legacy dict.

import pandas as pd
from rnamelt import (
    analyze_single, analyze_multi, analyze_concentration, analyze_csv,
)
from rnamelt.cleaning import clean

df = clean(pd.read_csv("melt.csv"))

analyze_single(df, "sample_1", struct_type="heterodimer", oligo=0.5)
analyze_multi(df, {"sample_1": 0.5, "sample_2": 5.0})
analyze_concentration(df, {"sample_1": 0.5, "sample_2": 5.0})

# read_csv + clean + dispatch in one call
analyze_csv("melt.csv", mode="single", column="sample_1", oligo=0.5)
analyze_csv("melt.csv", mode="concentration",
            oligo_multi={"sample_1": 0.5, "sample_2": 5.0})

Result dict shape

SingleResult.to_dict() (and analyze_single) return the same dict the in-browser pipeline produces:

{
  "name": "sample_1",
  "TmRaw": 52.5,
  "vantHoff":   {"success": True, "dH": ..., "dS": ..., "dG": ..., "T_m_vH": ...,
                 "fit_vh": (slope, intercept), ...},
  "fit_result": {"success": True, "dH": ..., "dS": ..., "dG": ..., "T_m_fit": ...,
                 "fit": [...], "derivative": [...],
                 "base_b_f": (m, b), "base_ub_f": (m, b)},
  "T_used": [...], "signal": [...],
  "base_b_r": (m, b), "base_ub_r": (m, b),
  ...
}

Concentration mode:

{
  "is_concentration":   True,
  "self_complementary": False,
  "per_curve": [
    {"name": ..., "TmRaw": ..., "TmvH": ..., "Tmfit": ..., "lnCT": ..., ...},
    ...
  ],
  "series": {
    "raw": {"dH": ..., "dS": ..., "dG_37": ..., "r_squared": ...,
            "slope": ..., "intercept": ..., ...},
    "vh":  {...},
    "fit": {...},
  },
  "skipped": [{"name": ..., "reason": ...}, ...],
}

ΔH is in kcal/mol, ΔS in kcal/(mol·K), Tm in °C (Kelvin variants suffixed _K). The typed result objects (SingleResult, MultiResult, ConcentrationResult, …) live in rnamelt.results and expose the same fields as attributes — numpy arrays stay as numpy arrays instead of being JSON-flattened.

Two-state model

The observed signal is a linear combination of folded and unfolded baselines, weighted by the fraction unfolded θ(T):

A(T) = (mF·T + bF)·(1 − θ) + (mU·T + bU)·θ
K(T) = exp(−ΔG / RT), ΔG = ΔH − T·ΔS
θ = K / (1 + K)     (monomer)
θ = (√(1 + 8·c0·K) − 1) / (4·c0·K)     (dimer)

Units throughout: ΔH in kcal/mol, ΔS in kcal/(mol·K), T in K (T_K = T_C − T0, with T0 = −273.15).