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
- Drag & drop a CSV onto the upload zone — or click to browse.
- Pick a signal column from the dropdown, or switch to
__multi__/__concentration__mode for multi-column analyses. - Use the dual-range slider to set the transition window
[T_low, T_high](°C). - Adjust the baseline offsets. Folded baseline is fit on
[T_low, T_low + lower offset], unfolded on[T_high − upper offset, T_high]. - Set strand concentration (µM) and salt (mM).
- 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
| Mode | Column dropdown | What 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:
| Action | Gesture |
|---|---|
| Zoom in / out | Mouse wheel scroll over the chart |
| Pan | Click and drag inside the chart area |
| Pinch zoom | Two-finger pinch on touch devices |
| Reset view | Click 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
| Flag | Default | Notes |
|---|---|---|
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-type | heterodimer | heterodimer · homodimer · monomer |
--signal-type | absorbance | absorbance · fluorescence |
--T-low / --T-high | data range | Transition window (°C) |
--bl-lower / --bl-upper | 10 | Baseline offsets (°C) |
--salt | 150 | NaCl in mM |
--oligo | 0.5 | Single-column strand concentration (µM) |
--oligo-multi NAME=VAL | — | Repeatable per-column µM for multi / concentration modes |
--csv-out | — | Write a results CSV |
--indent | 2 | JSON 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.
| Group | Flags |
|---|---|
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):
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).