Install

Requirements

Python ≥ 3.11, jax 0.5, equinox 0.13. Distribution and import name are the same: rheplicant.

Install

pip install rheplicant

limTOD comes with it. It is a dependency, not an extra — the sky engines it carries are the forward model rather than an accessory — and since limTOD 1.10.0 reached PyPI the >= 1.10 floor resolves with no preparatory step.

Extras

Two of the integration extras name a requirement that is not on PyPI, deliberately: the package is developed alongside them and pinning a git URL in pyproject.toml would make this project unpublishable. So the extra records what is needed, and you install it yourself.

Extra

Gives you

Install

numpyro

NUTS, and every gradient posterior

pip install "rheplicant[numpyro]"

cal

NoiseWaveOperator — the noise-wave receiver model, reflection couplings and all

pip install "rhino-cal-jax @ git+https://github.com/RHINO-Experiment/rhino-cal@feat/rhino-cal-jax" — the @feat/rhino-cal-jax is load-bearing: rhino_cal_jax/ exists only on that branch, and the default branch has no pyproject.toml to build

rfi

MomentRFIFlaggingOperator, the real flagger. The threshold-based FlaggingOperator needs none of it

pip install "MomentRFI @ git+https://github.com/zzhang0123/MomentRFI"

rhino

read_rhino_observation() — the RHINO HDF5 reader (h5py). The Touchstone reader needs none of it, being numpy only

pip install "rheplicant[rhino]"

uvbeam

Read pyuvdata UVBeam resources; the beam physics bridge itself comes with limTOD

pip install "rheplicant[uvbeam]"

gui

The packaged FastAPI + React configuration workbench and rheplicant-gui launcher

pip install "rheplicant[gui]", then rheplicant-gui; see the editor security and trust boundaries

Start the configuration workbench

The gui extra contains the production assets, so an installed wheel needs no Node.js toolchain:

pip install "rheplicant[gui]"
rheplicant-gui                    # http://127.0.0.1:8000/

The launcher binds to loopback by default. A non-loopback bind is refused unless --allow-remote is explicit, and that flag is only acknowledgement: the application has no authentication, tenant isolation or sandbox. YAML may load plugins and python: targets; resource/output fields are server paths; jobs use the server account’s files and compute. Read the complete workbench workflow and trust boundary before using remote access or running a document from another person.

Development

git clone https://github.com/RHINO-Experiment/rheplicant
cd rheplicant
uv venv
uv pip install -e . --group dev

Warning

Neither uv sync nor uv run works in this project — with --frozen or without it. Measured on a fresh clone, not assumed.

Without --frozen, each refuses with “your project’s requirements are unsatisfiable”. uv resolves every declared extra when it locks, and rheplicant[cal]rhino-cal-jax is not on PyPI by design; rheplicant[rfi]MomentRFI is the same shape. The pyproject.toml comment beside each says why they name a requirement instead of resolving it.

With --frozen, each refuses with “Unable to find lockfile at uv.lock. This repository ships no lockfile and cannot: uv lock fails on the very same unsatisfiable extra, so there is nothing to commit. --frozen applies only where a uv.lock already exists in your working copy — which a fresh clone does not have and cannot generate. Treat any uv.lock you find in an older checkout as stale rather than as the missing piece.

uv pip install resolves only what you ask it for, never the whole declared universe, which is why the two commands above work where the project-level ones cannot. Nothing is removed either, so editable local checkouts of limTOD or rhino-cal survive it — install those the usual way afterwards, in any order.

Optional local data

Two things this project compares itself against cannot be published: the RHINO CST far-field exports, and the rhino-cal checkout whose numpy readers are the reference implementation. Neither has a path that can be guessed, so neither is guessed — you name yours, or the work that needs it stands down and says so.

Variable

What it unlocks

RHEPLICANT_RHINO_BEAMS

The directory of per-frequency CST far-field exports. Unlocks the beam tests in tests/radio/test_beams.py, the real horn in examples/sky_to_noise_wave.py (which otherwise substitutes a Gaussian and labels the plot as such), and the receiver figure. --beam-dir overrides it for the example.

RHEPLICANT_RHINO_CAL

A rhino-cal checkout. Unlocks tests/radio/test_ingestion_vs_reference.py, which cross-checks this package’s Touchstone and HDF5 readers against rhino-cal’s.

Neither is required, and nothing fails without them — the tests that need them skip with the variable named in the reason. Both used to be hard-coded paths under one person’s home directory, which meant those tests never ran anywhere else and said nothing about it.

Running the tests

There is CI — .github/workflows/test.yml has run the suite on every push since d65785b (2026-08-24). Run the suite and the linter in the project venv anyway before pushing: the runner is Linux and this is often not, and the difference is not academic — several numeric assertions in this suite are currently red there and green here. The dev group carries what the tests need on top of the package — numpyro, because five modules import it unguarded, and mdit-py-plugins, because the docs-link guard computes anchors with myst’s own slugifier. Both are runtime extras rather than dependencies, so nothing but the test suite pulls them in; without them pytest stops at collection, or reports every documentation link as broken.

.venv/bin/python -m pytest                     # ~12 min with coverage
.venv/bin/python -m pytest -n 16 -o addopts="" # the one to use while working
JAX_ENABLE_X64=1 .venv/bin/python -m pytest tests/evidence   # the float64 half
.venv/bin/python -m ruff check src tests

The second line is the one to use while working. pytest-xdist is in the dev group and the run is identical — same counts — because nothing here depends on execution order.

How many workers, and why the number stops mattering. Measured on a 28-core machine: -n 8 183 s, -n 16 146 s, -n 24 150 s. The knee is around 16 and past it the workers contend rather than help, so -n auto on a big box is slower than picking a number. On a smaller machine use the core count; the suite does not care.

What sets the floor is one test, not the worker count. tests/evidence/ runs as a second pytest session with JAX_ENABLE_X64=1 (see the admonition above), and inside it test_chain_conditioning.py::test_the_accumulated_fisher_plus_the_prior_is_positive_definite[5.0-1000] alone takes ~49 s. That session is itself parallelised — four workers, chosen because 8 measured no faster and the parent is already an -n session — which took it from 175 s to 78 s and the whole suite from 291 s to 146 s. Nothing short of shrinking that one test moves the floor further.

Why parallel rather than fewer tests

The suite is not slow because it is large. Of a 692 s serial run, the four heaviest items account for 249 s; the other ~2700 tests come to roughly 290 s between them, a tenth of a second each. Deleting tests therefore buys almost nothing and costs coverage — and most of these tests are regression evidence for a specific failure, which is the last thing to trade for a minute.

The four are worth knowing, because they are where a slowdown would show up: the float64 subsession (159 s — a whole second pytest run, and the floor on any parallel time, since one worker must carry it alone), the beam-spill closure at two resolutions (70 s), the NPE training (28 s), and the tour executed as a script (16 s). Each verifies something nothing else can.

The second line is not optional work you might skip — plain pytest already runs it for you, in a subprocess, via tests/test_evidence_session.py. It is written out because that is how you run those tests directly when one of them fails.

Why the suite is two sessions

The evidence layer needs float64: a stored factor’s offset scalar is the time–bandwidth product, ~7.2e11 for one night, against a difference of ~1e5 — which float32 annihilates rather than rounds. The rest of the suite must stay at float32, because a population of tests assert refusals that only float32 forces — tests/test_evidence_session.py names them and the command that reproduces them. And jax_enable_x64 is process-global, so the two cannot share an interpreter.

That split is also why the reported coverage is what it is: the second session runs --no-cov in its own process, so its passing tests contribute nothing to the default report, and most of the default report’s uncovered statements are the seven evidence-layer files.

Check it worked

.venv/bin/python -c "import rheplicant; print(rheplicant.__version__)"
.venv/bin/python -c "from rheplicant.radio import RADIO_GRAPH; print(len(RADIO_GRAPH.nodes), 'nodes')"

The interpreter is named explicitly because nothing above activates the environment — a bare python here reaches whichever one is on your PATH and reports ModuleNotFoundError for an install that is in fact fine. Drop the prefix if you have run source .venv/bin/activate.

The second line is the more useful one: it proves the radio layer imported and the default signal-path template registered. If the extras are in place, these import too — each is the module an operator reaches for, and an absent one raises an ImportError naming what to install rather than failing later:

.venv/bin/python -c "import limtod_jax, rhino_cal_jax; print('sky engines and noise waves ready')"

Then read the guided tour, or ingestion if you have a recording in hand.