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 tested against a fixed commit of STILT-R. This section says what “matches STILT-R” covers and how to move to a newer STILT-R commit.

Pinned upstream commit#

Field

Value

Upstream

https://github.com/uataq/stilt

Pinned SHA

0e290a68730e155bc2156e28af492a463228c88a

Pin location

.github/workflows/tests.yml (env STILT_R_SHA)

Last verified

2026-06-25

When the fidelity tests pass, PYSTILT matches STILT-R at this commit. It says nothing about STILT-R’s current main branch.

What matches#

PYSTILT’s footprints match STILT-R’s for the 20 fidelity scenarios in tests/fixtures/r_stilt_reference.py, within these tolerances:

Quantity

Tolerance

Footprint, per cell

rtol=1e-7, atol=1e-8

Footprint total

rtol=1e-6

Footprint peak

rtol=1e-7

Number of nonzero cells

identical

Intermediate tables (interpolation, rtime, gridding)

rtol=1e-12

Trajectory positions and foot after the HNF correction

rtol=1e-7

Grid coordinates, longitude/latitude

atol=1e-12 degrees

Grid coordinates, projected

atol=1e-3 m

The scenarios cover:

  • point, column, and three-location multipoint receptors

  • 6 h and 24 h backward runs, and a 6 h forward run

  • the hyper-near-field (HNF) plume correction on (12 scenarios) and off

  • smooth_factor of 0, 0.5, 1, and 2

  • 0.01° and 0.05° longitude/latitude grids and a UTM grid

  • a 2° box, a receptor in the domain’s corner, a receptor at the edge of the met grid, and a small 0.1° domain that particles leave early

  • receptor heights above ground, above sea level (kmsl=1), and at 0.5 m

  • winter (stable) and summer (convective) HRRR

  • hourly footprints and time_integrate=True

  • a wind-error run (siguverr=1 m/s)

The synthetic tests in tests/r_stilt/test_footprint_synth.py feed hand-made particle tables to both implementations to test single code paths: one Gaussian particle, cells on the grid boundary, the dateline, a global grid, and the latitude scaling of the kernel width.

What does not match#

  • The NetCDF layout. STILT-R writes dimensions (x, y, time) with a fill value of -1. PYSTILT writes CF-1.8 NetCDF with dimensions (time, lat, lon). Read PYSTILT files as ordinary CF NetCDF. Tools that expect STILT-R’s layout will not read them.

  • Forward runs with the HNF correction. STILT-R accumulates the plume from the far end of a forward trajectory back toward the release point, so its plume is widest at release. PYSTILT accumulates outward from the release point. The two foot values differ on purpose, and test_forward_hnf_foot_intentionally_differs_from_r guards the difference.

  • Untested inputs. Runs longer than 24 h backward, latitudes above 80°, and grids finer than 0.001° have not been compared.

  • Other STILT-R commits. Nothing detects upstream changes to calc_footprint.r, permute.f90, or calc_trajectory.r. Bump the pinned SHA by hand (below).

Large-scale comparison#

Outside CI, PYSTILT was compared with STILT-R on 200 receptors at the WBB tower in the Salt Lake Valley, 2016 to 2024 (35 m above ground, 1000 particles, 24 h backward, HRRR, krand=2 and seed=42, the same v5.1.0 hycs_std on both sides). For each receptor, the PYSTILT trajectory was compared with a separate STILT-R calc_trajectory run, and the PYSTILT footprint with STILT-R’s calc_footprint of the same particles, at 0.01°, 0.05°, and 0.1°. All 200 receptors matched.

Quantity

Agreement (worst of 200 receptors)

Trajectory, per particle

absolute 1.0e-17, relative 5.6e-16 (float64 round-off)

Footprint, per-cell relative difference

median ~2e-8, max ~6e-8

Footprint nonzero cells

identical (for example 250,644 cells at 0.01°)

Footprint total

relative ~2e-8

The trajectories are identical, so the footprint differences come only from the order in which floating-point sums are added. The relative difference is the same for small cells (under 0.01 % of the peak) as for large ones. Footprints are stored as float32, whose precision is about 1.2e-7, so the two outputs agree to the precision the file can hold. The mass-weighted relative difference, which is what an inversion sums over, is also about 2e-8.

Reading the same met files on both sides over the full date range needs the find_met_files fix in the pinned STILT-R commit. HRRR archives can hold the same file both at the top level (as a symlink) and in a YYYY/MM/ subdirectory, and older STILT-R listed it twice.

Moving to a newer STILT-R commit#

  1. Change STILT_R_SHA in .github/workflows/tests.yml.

  2. Run the fidelity tests 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
    
  3. If every scenario passes at the current tolerances, commit the change and update “Last verified” above.

  4. If a scenario fails, diff calc_footprint.r, permute.f90, and calc_trajectory.r between the two commits to find the upstream change. Then do one of the following:

    • make the same change in PYSTILT,

    • loosen that scenario’s tolerance, with a comment saying why, or

    • stay on the old commit.

Files to diff on every bump#

These STILT-R files determine the numbers PYSTILT is compared against:

  • r/src/calc_footprint.r: footprint kernel and gridding

  • r/src/calc_trajectory.r: the HYSPLIT wrapper, random seed, and krand

  • r/src/permute.f90: the Fortran loop that adds each particle’s kernel to the grid

The R helper scripts in tests/fixtures/r_helpers/ call these files for the synthetic tests. They may need updating if a function signature changes upstream.