JavaScript port of Rangefinder: automated ranging, species identification, and composition for atom probe tomography (APT) mass spectra, running entirely in Node.js or in the browser.
Live browser app · MIT license · Build status
The port mirrors the Python pipeline stage by stage and is verified against a
frozen baseline of the Python package (see
test/python-baseline/BASELINE.md, PyPI release 0.2.1). On all four
public controls the outputs match the Python reference:
| control | peaks | peak positions | peak areas | at.% | isotope fractions |
|---|---|---|---|---|---|
| Si (APAV) | 30/30 | < 1e-11 Da | < 4e-8 rel | exact* | exact* |
| ODS steel (Wang) | 55/55 | < 3e-12 Da | < 2e-8 rel | exact* | exact* |
| Ck10 steel (Felfer) | 30/30 | < 3e-11 Da | < 1e-8 rel | exact* | exact* |
| MoHf (Leitner) | 47/47 | < 7e-12 Da | < 3e-8 rel | exact* | exact* |
* identical to comparison precision (1e-4 at.%, 1e-5 fraction); top species match on every peak.
Residual differences come from optimizer convergence paths (the TRF least-squares and L-BFGS-B ports converge to the same minima but not bit-identically). One class of divergence to know about: when two histogram bins are mathematically tied, the winner is decided by float noise; the JS Savitzky-Golay uses exact integer arithmetic so true ties resolve as plateaus (matching scipy's plateau midpoints).
Ported: POS loading, multi-resolution detection, crowded-window mixture deblending, atomic + molecular family fingerprint recovery, adaptive ranging (shadow EER), significance gate, joint-optimizer assignment with element pruning, overlap deconvolution, composition/species/isotope tables, isotope anomaly table (raw, un-reconciled), and fixed-ranging regional composition when the caller supplies an event label for every ion. Regional element, species, and isotope counts are checked to recombine with the whole-sample solution.
Not ported (yet): the isotope audit (EMG tail refits; only affects anomaly
reconciliation), spatial label detection, spatial stability diagnostics, the
two-pass assignment backend, plotting, and the benchmark harness. .epos files
are not supported.
git clone https://github.com/kylemcdonald/rangefinder.js.git
cd rangefinder.js
npm test
node cli/run.js sample.pos out/ # analyze, write JSON outputs
node cli/compare.js <python-ref-dir> out/ # diff against a Python reference runThere are no runtime npm dependencies.
The parity/worker tests expect the control .pos files at
../rangefinder/controls/ (they skip if absent).
Open the hosted app, then
drop a .pos or .cpos file anywhere on the page. The file is decoded and
analyzed locally in a Web Worker and is never uploaded to a server. Drop an
.rrng file to make its expert ranges available, or drop matching data and
.rrng files together. A dropped RRNG becomes active immediately. Paired
files skip the automatic analysis for a faster initial load; the Use
rangefinder setting runs Rangefinder explicitly when desired.
CPOS beta files are expanded to their original ion count. Their rare bins retain complete 12-bit tuples, common bins use the stored 0.002 Da mass distribution, and the quick RRNG path reads that source spectrum directly. Only the current 224-byte beta layout is accepted; earlier CPOS layouts have no fallback decoder.
To run the app locally, serve the repository root (the app imports ../src/*
from web/):
python3 -m http.server 8000
# open http://localhost:8000/Analysis runs with a progress bar; when done you get:
- center — the reconstruction as a WebGL point cloud (4M-point render
budget, automatic point opacity with a manual override, manual point size,
translucent rendering with a dimmed haze
for out-of-range ions, nm-tick bounds guide; double-click resets camera).
The budget is allocated per peak group by waterfilling (
web/budget.js): rare groups keep every ion and only the most abundant peaks (and the background) are uniformly thinned, with per-group alpha compensation so perceived density stays faithful — selecting a 6K-ion trace element always shows all of its ions. The coloring follows the active table tab: one saturated color per row (elements blend for multi-element species; the isotope view colors only the dominant-charge atomic families). Selecting rows hides points outside the selection by default; their opacity can be raised independently; - bottom — the full spectrum (wheel = zoom, drag = brush a m/z window that filters the 3D view, drag inside the window = move it, drag the y-axis = log↔linear, double-click = reset, hover for counts + nearest species);
- right — Elements / Species / Isotopes tables; clicking rows toggles them and highlights the matching peak regions in the spectrum and the matching points in 3D (multi-select, select all, deselect all, and single-row arrow-key stepping supported). The Settings panel exposes point size, automatic/manual opacity, perspective/orthographic projection, continuous Composition Contrast, outside-selection opacity, and additive blending. Settings persist locally across reloads. The Slice panel clips the reconstruction independently along x, y, and z, with a flip control for choosing the retained side. Point rendering always leaves depth testing off. Each composition tab remembers its own row selection;
- top — workspace PNG, RRNG, and three-table CSV exports, plus independent
visibility controls for the Settings and Composition panels. CSV
countsare deterministic integer apportionments of the probabilistic composition; fractional expected values are retained separately asweighted_counts. Exported RRNG files retain full-precision boundaries and Rangefinder identity/composition metadata, so re-importing them with the same POS restores the same result while remaining readable by standard RRNG tools.
The Python package is under active development. The workflow:
- Freeze the new Python state: update
test/python-baseline/(git archive) andBASELINE.md. - Regenerate references:
test/scripts/gen_reference.pyper control (run with the baseline onPYTHONPATH,enable_segmentation=False). - Re-run
node cli/run.js+node cli/compare.jsper control; adapt the JS (usuallysrc/config.jsfor new tuning constants, plus whatever stage changed) until everything passes. npm testmust stay green;test/scripts/gen_fixtures.pyregenerates the scipy fixtures if the numeric primitives ever change.
src/config.js mirrors config/defaults.yaml and must be kept in sync by
hand — a config-only change in Python usually needs only that file.
src/lib/— numeric primitives ported from scipy/numpy: filters (savgol/gaussian/median), peak finding (find_peaks/prominences/widths), NNLS, bounded trust-region least squares (TRF + soft_l1), projected L-BFGS-B, small-matrix SVD/rank/cond/pinv.src/analysis/— the pipeline stages, file-for-file mirrors ofrangefinder/analysis/*.py.src/data/isotopes.data.js— NIST isotope table generated from the Python package (test/scripts/gen_isotopes.py).cli/— node runner + reference comparator.web/— the browser app (worker, spectrum panel, three.js scene, tables).test/— scipy fixtures, parity tests, frozen Python baseline + reference outputs.