Development#
Contributions are welcome: bug reports, documentation fixes, and code. See CONTRIBUTING.md for conventions, and open issues on GitHub.
Use of AI coding agents#
This project is developed with the help of AI coding agents, directed and reviewed by the maintainer, who owns the design and the science.
If you contribute with an agent, AGENTS.md at the repository root is the
orientation file it should read.
Set up a development environment#
PYSTILT uses uv and just. From a clone of the repository:
uv sync --group dev
Common tasks:
just test # run the test suite
just quality-check # lint, type-check, and test
just build-docs # build this documentation into docs/_build/html
STILT-R parity#
PYSTILT is validated against the uataq/stilt R implementation at a pinned upstream commit. This section records the scope of the parity claim and the procedure for keeping it current.
Pinned upstream commit#
Field |
Value |
|---|---|
Upstream |
|
Pinned SHA |
|
Pin location |
|
Last verified |
2026-06-25 |
The SHA is the single source of truth for what “matches STILT-R” means in
this repository. Reviewers reading “tests pass” should interpret it as
tests pass against this exact commit, not the upstream main branch.
Scope of the parity claim#
PYSTILT matches STILT-R on numerical footprint values for the 20 fidelity scenarios in tests/fixtures/r_stilt_reference.py:
Quantity |
Tolerance |
|---|---|
Per-cell footprint |
|
Total footprint sum |
|
Peak footprint |
|
Intermediate tables (interpolation, rtime, gridding) |
|
Trajectory positions + foot column after HNF |
|
Coord arrays (longlat) |
|
Coord arrays (projected meters) |
|
What is not in scope:
NetCDF wire format: STILT-R writes dims
(x, y, time)withfill_value=-1; PYSTILT writes(time, lat, lon)with xarray defaults. Downstream R-based readers that expect the STILT-R byte layout are not supported; PYSTILT output should be read as a generic CF-1.8 NetCDF.Behavior on inputs outside the tested scenarios – see “What is tested” below. Untested scenarios (long-duration backward >24h, polar latitudes >80°, very fine grids <0.001°, etc.) are unverified, not known to match.
STILT-R versions other than the pinned SHA. Upstream changes to
calc_footprint.r,permute.f90, orcalc_trajectory.rare not automatically detected; the SHA must be bumped manually.
What is tested#
The 20 fidelity scenarios in tests/fixtures/r_stilt_reference.py cover:
Receptor types: point, column, three-location multipoint
Direction: 6h backward, 24h backward, 6h forward (
time_sign=+1)HNF plume dilution: on (12 scenarios) and off
Smoothing:
smooth_factorin {0.0, 0.5, 1.0, 2.0}Grids: 0.01° longlat, 0.05° coarse longlat, UTM projected
Domains: standard 2° box, edge-receptor (SW corner), met-grid edge, tight sparse 0.1° (particles exit early), dateline-crossing (synthetic test), global 360° (synthetic test)
Altitude refs: AGL, MSL (
kmsl=1), low AGL (0.5 m)Meteorology: winter HRRR (stable PBL), summer HRRR (convective PBL)
Time integration: hourly bins and
time_integrate=TrueError trajectory: WINDERR with
siguverr=1 m/s
Additional synthetic tests in tests/r_stilt/test_footprint_synth.py exercise hand-crafted particle DataFrames against STILT-R to isolate specific code paths (single-particle Gaussian, boundary fenceposts, dateline crossing, global grid, latitude bandwidth scaling, etc.).
Large-scale seed-matched validation#
Beyond the 20 CI fidelity scenarios, PYSTILT has been validated against
STILT-R on a 200-receptor, seed-matched, bit-for-bit campaign spanning
2016-2024 (WBB tower, Salt Lake Valley; 35 m AGL; 1000 particles; 24 h
backward; HRRR; krand=2, seed=42; byte-identical v5.1.0
hycs_std on both sides). For each receptor the PYSTILT trajectory is
compared to an independent STILT-R calc_trajectory run, and the PYSTILT
footprint to R’s calc_footprint of the same particles, at three
resolutions (0.01°, 0.05°, 0.1°).
Result: all 200 receptors match bit-for-bit.
Quantity |
Agreement (max over 200 receptors) |
|---|---|
Trajectory, per particle |
abs |
Footprint, per-cell relative error |
median |
Footprint nonzero-cell set |
identical (e.g. 250,644 = 250,644 at 0.01°) |
Footprint total mass |
rel |
The per-cell relative error is uniform across cell magnitudes – the smallest cells (<0.01% of peak) agree as tightly as the largest, so small footprint values are not disproportionately wrong. This follows from the trajectories being bit-identical: the only footprint difference is float-summation order in the gridding/convolution, and footprints are stored as ``float32`` (eps ~ 1.2e-7). The observed ~2e-8 relative agreement is therefore below the float32 storage floor – the two outputs agree to the precision the NetCDF format can represent. Mass-weighted relative error (what a flux inversion integrates) is ~2e-8.
This is a one-time field-scale validation (not run in CI); it complements
the structural coverage of the CI fidelity scenarios with broad
real-meteorology breadth. Reading identical met on both sides across the
full date range requires the find_met_files dedup fix carried in the
pinned SHA – HRRR archives can expose one physical file via both a
top-level symlink and a YYYY/MM/ subdirectory, which older STILT-R
listed twice.
Procedure for bumping the pinned SHA#
Bump
STILT_R_SHAin.github/workflows/tests.yml.Locally:
git clone https://github.com/uataq/stilt stilt-r-src git -C stilt-r-src checkout <new-sha> STILT_R_DIR=$PWD/stilt-r-src uv run pytest tests/r_stilt/ -v -m integration
If all fidelity scenarios pass at the existing tolerances, commit the bump and update the “Last verified” date in this section.
If a scenario fails, diff
calc_footprint.r,permute.f90, andcalc_trajectory.rbetween the old and new SHA, identify the upstream change, and decide:Mirror the change in PYSTILT, or
Loosen the affected scenario’s tolerance with a comment explaining why, or
Hold the pin at the previous SHA.
Files watched for upstream drift#
These three STILT-R files materially influence the numerical output and should be diffed on every SHA bump:
r/src/calc_footprint.r– footprint construction, kernel, griddingr/src/calc_trajectory.r– HYSPLIT wrapper, RNG seeding, KRANDr/src/permute.f90– Fortran convolution accumulator
Helper R scripts under tests/fixtures/r_helpers/ re-export these for
the synthetic comparison tests; bumping the SHA may require updating those
if upstream signatures change.