Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
23f6324
Fix env-CAPE flag loss in pi() (#78)
dgilford Jul 11, 2026
8ec698e
Add period-2 oscillation rescue to the pressure solver
dgilford Jul 11, 2026
5ea03b4
Convert sample humidity to true mixing ratio; CF-fix month attrs (#60)
dgilford Jul 11, 2026
c6cc493
Document CL sensitivity (2500 vs physical 4190)
dgilford Jul 11, 2026
63b2924
Add native whole-field pi_field fast path (#86)
dgilford Jul 11, 2026
9af4631
Add ruff/mypy tooling config, CI lint job, and numba graceful fallback
dgilford Jul 11, 2026
2f77bf5
Apply ruff formatting and lint fixes (mechanical)
dgilford Jul 11, 2026
68c69cf
Update pixi.lock for lint environment
dgilford Jul 11, 2026
bdaba81
Packaging: PyPI readme links, Python 3.11 floor, main-rename prep, co…
dgilford Jul 11, 2026
ec3df61
Ignore local issue-replies working file
dgilford Jul 11, 2026
ce66c8b
Apply AI-review fixes: dedup pi/pi_field wrappers, keyword-only pi_fi…
dgilford Jul 11, 2026
7fee26d
Apply hygiene-review fixes: README ds.r, lean sdist allowlist, link/n…
dgilford Jul 11, 2026
d4aa131
Refine IFL docstring per science review: rescue amplitude coverage, p…
dgilford Jul 11, 2026
848a85d
Speed up kernels bit-identically: scalar min/max, contiguous pi_field…
dgilford Jul 11, 2026
2e7a2de
Hoist per-column environment preprocessing out of the CAPE calls (bit…
dgilford Jul 11, 2026
70c3520
Document kernel bit-identity policy and session workflows in AGENTS.md
dgilford Jul 11, 2026
ecd4d10
Revert "Hoist per-column environment preprocessing out of the CAPE ca…
dgilford Jul 11, 2026
a99d798
Regenerate MATLAB PI reference on the r-converted 2024 sample
dgilford Jul 11, 2026
d144f30
Rework the clock notebook Skyborn comparison into an apples-to-apples…
dgilford Jul 11, 2026
1525154
Refresh notebook outputs on the tcpyPI pixi kernels; pin notebook ker…
dgilford Jul 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion .github/workflows/pytest.yaml
Original file line number Diff line number Diff line change
@@ -1,13 +1,36 @@
name: Run tests

# Triggers cover both `master` and `main` so CI keeps working through the
# default-branch rename (master -> main); drop `master` after the rename.
on:
push:
branches:
- master
- main
- master
pull_request:

jobs:
lint:
name: lint (ruff + mypy)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: prefix-dev/setup-pixi@v0.9.1
with:
environments: lint

- name: ruff check
run: pixi run -e lint lint

- name: ruff format --check
run: pixi run -e lint format-check

- name: mypy
run: pixi run -e lint typecheck

test:
name: ${{ matrix.os }}-${{ matrix.dependencies }}
runs-on: ${{ matrix.os }}
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,7 @@ papers/
SESSION_NOTES.md
REVIEW_2026-07.md
MCP_PLAN.md
ISSUE_REPLIES.md

# Local AI-session handoff notes (not tracked)
.ai/
10 changes: 10 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Contributor convenience: `pip install pre-commit && pre-commit install`.
# CI enforces the same checks via the `lint` job (ruff check + format + mypy).
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Keep close to the CI lint env's ruff so pre-commit and CI agree on formatting.
rev: v0.15.21
hooks:
- id: ruff-check
args: [--fix]
- id: ruff-format
36 changes: 20 additions & 16 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,36 @@ Guidance for AI agents and human contributors working in this repo. (`CLAUDE.md`

## Project Structure & Module Organization
Core library in `src/tcpyPI/`:
- `pi.py` — the BE02 PI algorithm (`pi()`, `cape()`, entropy solver). Numba-compiled.
- `utilities.py` — thermodynamic helpers, unit/longitude conversions, PI efficiency/disequilibrium.
- `constants.py` — meteorological constants; `numba.py` — the `@njit` wrapper.
- `analyses.py` — Wing et al. (2015) PI log-decomposition API.
- `vi.py` — TE12 ventilation index + entropy-deficit (`chi_m`). `gpi.py` — genesis potential index (`en04`, `e10`). `pdi.py` — power dissipation index. `relative_intensity.py` — ν = V/V_PI. (VI/GPI/PDI are experimental.)
- `pi.py` — the BE02 PI algorithm (`pi()`, `cape()`, entropy solver), the whole-field fast path `pi_field()` (parallel guvectorize; ~2 orders of magnitude over per-column loops), and the Wing et al. (2015) log-decomposition `pi_log_decomposition()`. Numba-compiled. `cape()` keeps the original pcmin.m per-call structure line-for-line (an environment-setup hoist was certified bit-identical but reverted: negligible speed gain did not justify structural drift from the MATLAB reference).
- `utilities.py` — thermodynamic helpers, unit/longitude conversions, PI efficiency/disequilibrium (`decompose_pi` scalar core).
- `constants.py` — meteorological constants (fully documented, incl. the deliberate modified `CL=2500`); `numba.py` — the `@njit`/`guvectorize` wrapper (graceful pure-Python fallback when Numba is absent).
- `vi.py` — TE12 ventilation index + entropy-deficit (`chi_m`) + `vi_log_decomposition`. `gpi.py` — genesis potential index (`en04`, `e10`) + `gpi_log_decomposition`. `pdi.py` — power dissipation index. `relative_intensity.py` — ν = V/V_PI. (VI/GPI/PDI are experimental.)

Sample workflow in `run_sample.py` (writes `data/raw_sample_output.nc`, `data/full_sample_output.nc`). Tests in `tests/`. Notebooks in `notebooks/`; figures in `figures/`; legacy BE02 MATLAB in `matlab_scripts/`.

## Build, Test, and Development Commands
- `pip install -e .` (Python 3.10+). Optional extras: `.[xarray]`, `.[demo]`.
- Tests use pixi. **Note:** a stale `pixi` shim may shadow the real binary — use `~/.pixi/bin/pixi` if `pixi` fails with a module error.
- `pixi run -e test-latest basic-tests` — core suite + doctests (49 tests).
- `pixi run -e test-xarray-latest xarray-tests` — sample-output regression (2 tests).
- `pixi run -e test-min basic-tests` — minimum supported floor (py3.10 / numpy 1.26 / numba 0.59).
- `pip install -e .` (Python 3.11+). Optional extras: `.[xarray]`, `.[demo]`.
- Tests and tooling use pixi. **Note:** a stale `pixi` shim may shadow the real binary — use `~/.pixi/bin/pixi` if `pixi` fails with a module error.
- `pixi run -e test-latest basic-tests` — core suite + doctests.
- `pixi run -e test-xarray-latest xarray-tests` — sample-output regression (rtol=1e-13 pins).
- `pixi run -e test-min basic-tests` — minimum supported floor (py3.11 / numpy 1.26 / numba 0.59).
- Lint/format/type-check (enforced by the CI `lint` job on every push/PR):
- `pixi run -e lint lint` (ruff check) · `pixi run -e lint format-check` / `format` (ruff format) · `pixi run -e lint typecheck` (mypy).
- Contributors can also `pip install pre-commit && pre-commit install` (ruff + ruff-format hooks).
- Data: `pixi run download-era5` (needs a CDS API key in `~/.cdsapirc`) then `pixi run build-sample` regenerates `data/sample_data.nc` + `data/era5_demo_subset.nc`. `python run_sample.py` regenerates the output artifacts.
- Benchmarks: the `bench` pixi env adds Skyborn for the optional comparison in `notebooks/clock_pypi.ipynb`. To run that notebook in JupyterHub, register the env as a kernel: `pixi run -e bench python -m ipykernel install --user --name tcpypi-bench --display-name "tcpyPI (bench)"`.

## Data Policy
- Track only the small samples (`sample_data.nc` ~6 MB, `era5_demo_subset.nc`, run_sample outputs). `.gitignore` uses an explicit allowlist — do **not** commit raw ERA5 downloads or other large `.nc`.
- The tracked sample is regional (North Atlantic), subsampled to ~2° (decimation, no interpolation), SST masked to ocean. Regenerate a wider/global sample by widening the `area` in `data/download_era5.py`.
- Track only the small samples (`sample_data.nc` ~6 MB, `era5_demo_subset.nc`, run_sample outputs, the MATLAB reference). `.gitignore` uses an explicit allowlist — do **not** commit raw ERA5 downloads or other large `.nc`.
- The tracked sample is regional (North Atlantic 2024), subsampled to ~2° (decimation, no interpolation), SST masked to ocean; humidity is stored as the **true water-vapor mixing ratio `r`** (converted from ERA5 specific humidity via `r = q/(1−q)`), with ERA5 relative humidity as `rh`. Regenerate a wider/global sample by widening the `area` in `data/download_era5.py`.

## Coding Style & Scientific Invariants
- PEP 8, 4-space indent, snake_case. Document units for scientific quantities; prefer keyword args for flags.
- PEP 8 via ruff (line length 100); snake_case. Document units for scientific quantities; prefer keyword-only args for flags on new APIs (see `pi_field`).
- **Never enable Numba `fastmath`** anywhere: the PI/CAPE missing-value logic relies on IEEE NaN semantics (`NaN > 0` is False, min/max NaN propagation). `@njit` already defaults to `cache=True` (see `numba.py`); don't remove it.
- `pi()` expects SST/T in °C, MSL/P in hPa, mixing ratio in g/kg, profiles ordered surface→top (the wrapper now sorts to descending pressure, so any order is accepted).
- Keep changes test-pinned: `pi()` doctests pin outputs to ~13 sig figs; `tests/test_run_sample.py` pins the sample outputs at `rtol=1e-13`.
- **Kernel-change policy (bit-for-bit identicality):** the compiled PI/CAPE kernels implement the published GMD-2021/pcmin algorithm and any change to them must be *math-by-math identical* — proven, not asserted. Acceptable transforms: selections (scalar `min`/`max` with preserved argument order), hoisting/deduplicating pure computations, memory-layout changes. **Never** reorder/reassociate arithmetic. Certify with the byte-equality harness (`.ai/bit_identity_harness.py`: old-commit worktree vs working tree over the full sample × option combos through both entry points, direct `cape()` calls, and a 50k fuzz corpus), and re-certify on the final post-`ruff format` bytes.
- `pi()`/`pi_field()` expect SST/T in °C, MSL/P in hPa, mixing ratio in g/kg; any vertical profile order is accepted (sorted internally to decreasing pressure). NaN P/MSL → `IFL=3`; NaN SST → `IFL=0`; period-2 solver oscillations with amplitude ≲1 hPa are rescued to the cycle midpoint (`IFL=1`; documented divergence from pcmin.m, which returns missing for those soundings). Flags tripped by the environmental-CAPE call persist (#78), so `IFL∈{0,3}` can accompany finite outputs — **`IFL==1` is the trustworthy-output gate**.
- Keep changes test-pinned: `pi()` doctests pin outputs to ~13 sig figs; `tests/test_run_sample.py` pins the sample outputs at `rtol=1e-13`; `tests/test_oscillation_rescue.py` pins bit-identity of a converging control profile; `tests/test_pi_field.py` pins `pi_field == pi` exact equality.

## Commit & Pull Request Guidelines
- Imperative commit summaries; group related changes. Reference issues/PRs; note any data or API changes.
- Run `basic-tests` (and `xarray-tests` when touching `run_sample.py`/data) before pushing. Do not commit large binaries.
- Before pushing, run `basic-tests`, the lint tasks, and (when touching `run_sample.py`/data) `xarray-tests`. Do not commit large binaries.
20 changes: 13 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ The goals in developing and maintaining pyPI are to:
* carefully document the BE02 algorithm and its Python implementation, and to
* demonstrate and encourage the use of potential intensity theory in tropical cyclone climatology analysis.

If you have any questions, comments, or feedback, please [contact the developer](mailto:dgilford@climatecentral.org) or open an [Issue](https://github.com/dgilford/pyPI/issues) in the repository. A paper detailing pyPI is published [at Geoscientific Model Development](https://gmd.copernicus.org/articles/14/2351/2021/gmd-14-2351-2021.pdf).
If you have any questions, comments, or feedback, please [contact the developer](mailto:dgilford@climatecentral.org) or open an [Issue](https://github.com/dgilford/tcpyPI/issues) in the repository. A paper detailing pyPI is published [at Geoscientific Model Development](https://gmd.copernicus.org/articles/14/2351/2021/gmd-14-2351-2021.pdf).

## Citation
pyPI was developed by [Daniel Gilford](https://github.com/dgilford) and is archived on Zenodo. A machine-readable [`CITATION.cff`](CITATION.cff) is included.
Expand All @@ -39,21 +39,26 @@ The description includes the pyPI background, a PI computation derivation, valid

## Getting Started

pyPI requires **Python 3.10+** to run.
pyPI requires **Python 3.11+** to run.
To get pyPI up and running on your system, clone the repository and ensure that you have the required dependencies.

### Installation

pyPI is packaged using the python package manager [pip](https://pip.pypa.io/en/stable/).

[![PyPI version](https://badge.fury.io/py/tcpypi.svg)](https://badge.fury.io/py/tcpypi)
[![conda-forge](https://img.shields.io/conda/vn/conda-forge/tcpypi.svg)](https://anaconda.org/conda-forge/tcpypi)

To install tcpypi from the command line:
Install from [PyPI](https://pypi.org/project/tcpypi/):

```bash
pip install tcpypi
```

or from [conda-forge](https://anaconda.org/conda-forge/tcpypi):

```bash
conda install -c conda-forge tcpypi
```

### tcpyPI Dependencies

Core (installed automatically): [NumPy](https://numpy.org/) (`>=1.26`) and [Numba](http://numba.pydata.org/) (`>=0.59`).
Expand Down Expand Up @@ -93,7 +98,7 @@ from tcpyPI import pi

ds = xr.open_dataset("data/sample_data.nc") # dims: month, p, lat, lon
vmax, pmin, ifl, t0, otl = xr.apply_ufunc(
pi, ds.sst, ds.msl, ds.p, ds.t, ds.q,
pi, ds.sst, ds.msl, ds.p, ds.t, ds.r,
kwargs=dict(CKCD=0.9),
input_core_dims=[[], [], ["p"], ["p"], ["p"]],
output_core_dims=[[], [], [], [], []],
Expand Down Expand Up @@ -289,11 +294,12 @@ and examine the outputs locally produced in [full_sample_output.nc](./data/full_
* **[run_sample.py](run_sample.py)** - Example script that computes PI and accompanying analyses over the entire sample dataset

#### Data
* [sample_data.nc](./data/sample_data.nc) - Sample state variables from ERA5 2024 monthly means over the North Atlantic (12 months; ~2° subsample; SST masked to ocean). Includes the core PI inputs (SST, MSL, T, q) plus winds, RH, and vorticity for the GPI/ventilation-index analyses. Built by [data/build_sample_data_era5_oct2024.py](./data/build_sample_data_era5_oct2024.py) from the (untracked, regenerable) ERA5 downloads.
* [sample_data.nc](./data/sample_data.nc) - Sample state variables from ERA5 2024 monthly means over the North Atlantic (12 months; ~2° subsample; SST masked to ocean). Includes the core PI inputs (SST, MSL, T, and the true water-vapor mixing ratio `r = q/(1−q)` converted from ERA5 specific humidity) plus winds, RH, and vorticity for the GPI/ventilation-index analyses. Built by [data/build_sample_data_era5_oct2024.py](./data/build_sample_data_era5_oct2024.py) from the (untracked, regenerable) ERA5 downloads.
* [era5_demo_subset.nc](./data/era5_demo_subset.nc) - Small single-column ERA5 environment (Hurricane Milton 2024 reference location) used by the ventilation-index and GPI demo notebooks.
* [mdr.json](./data/mdr.json) - Main Development Region definitions from [Gilford et al. (2017)](https://journals.ametsoc.org/doi/abs/10.1175/JCLI-D-16-0827.1)
* [raw_sample_output.nc](./data/raw_sample_output.nc) - Sample outputs from pi.py *only*, created by run_sample.py
* [full_sample_output.nc](./data/full_sample_output.nc) - Full set of sample outputs from pi.py plus analyses (efficiency, disequilibrium, log decomposition)
* [matlab_pi_reference_2024.nc](./data/matlab_pi_reference_2024.nc) - BE02 MATLAB (pc_min) reference outputs for the 2024 sample, generated by [reference_calculations.m](./matlab_scripts/reference_calculations.m) and used by verify_pi.ipynb

#### Validation and Testing Notebooks
* **[test_pi_calc.ipynb](./notebooks/test_pi_calc.ipynb)** - Single call of pi.py on the 2024 sample plus a quick run-time check
Expand Down
Loading
Loading