ppgrid 0.2.2__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. ppgrid-0.3.0/.github/workflows/ci.yml +22 -0
  2. ppgrid-0.3.0/AGENTS.md +143 -0
  3. ppgrid-0.3.0/CHANGELOG.md +91 -0
  4. {ppgrid-0.2.2 → ppgrid-0.3.0}/PKG-INFO +1 -1
  5. ppgrid-0.3.0/bench/turbo_bench.py +69 -0
  6. ppgrid-0.3.0/bench/turbo_decomp.py +96 -0
  7. ppgrid-0.3.0/docs/turbo-design.md +1168 -0
  8. ppgrid-0.3.0/ppgrid/_prof.py +75 -0
  9. {ppgrid-0.2.2 → ppgrid-0.3.0}/ppgrid/calibrate.py +31 -2
  10. ppgrid-0.3.0/ppgrid/pipeline.py +2703 -0
  11. ppgrid-0.3.0/ppgrid/pullpush.py +1000 -0
  12. ppgrid-0.3.0/ppgrid/turbop.py +870 -0
  13. ppgrid-0.3.0/ppgrid/zstdmt.py +592 -0
  14. {ppgrid-0.2.2 → ppgrid-0.3.0}/pyproject.toml +12 -1
  15. {ppgrid-0.2.2 → ppgrid-0.3.0}/tests/test_build.py +1 -0
  16. {ppgrid-0.2.2 → ppgrid-0.3.0}/tests/test_calibrate.py +44 -0
  17. ppgrid-0.3.0/tests/test_pipeline.py +883 -0
  18. ppgrid-0.3.0/tests/test_pullpush_mt.py +371 -0
  19. {ppgrid-0.2.2 → ppgrid-0.3.0}/tests/test_tier2_shared.py +1 -1
  20. ppgrid-0.3.0/tests/test_turbo.py +877 -0
  21. ppgrid-0.3.0/tests/test_turbop.py +513 -0
  22. ppgrid-0.3.0/tests/test_zstdmt.py +357 -0
  23. {ppgrid-0.2.2 → ppgrid-0.3.0}/uv.lock +1 -1
  24. ppgrid-0.2.2/.github/workflows/ci.yml +0 -18
  25. ppgrid-0.2.2/AGENTS.md +0 -94
  26. ppgrid-0.2.2/CHANGELOG.md +0 -65
  27. ppgrid-0.2.2/ppgrid/pipeline.py +0 -1077
  28. ppgrid-0.2.2/ppgrid/pullpush.py +0 -443
  29. ppgrid-0.2.2/tests/test_pipeline.py +0 -423
  30. {ppgrid-0.2.2 → ppgrid-0.3.0}/.github/CODEOWNERS +0 -0
  31. {ppgrid-0.2.2 → ppgrid-0.3.0}/.github/workflows/publish.yml +0 -0
  32. {ppgrid-0.2.2 → ppgrid-0.3.0}/.gitignore +0 -0
  33. {ppgrid-0.2.2 → ppgrid-0.3.0}/.pre-commit-config.yaml +0 -0
  34. {ppgrid-0.2.2 → ppgrid-0.3.0}/LICENSE +0 -0
  35. {ppgrid-0.2.2 → ppgrid-0.3.0}/README.md +0 -0
  36. {ppgrid-0.2.2 → ppgrid-0.3.0}/SECURITY.md +0 -0
  37. {ppgrid-0.2.2 → ppgrid-0.3.0}/SUPPORT.md +0 -0
  38. {ppgrid-0.2.2 → ppgrid-0.3.0}/bench/bench_tier2.py +0 -0
  39. {ppgrid-0.2.2 → ppgrid-0.3.0}/bench/compare_tier2.py +0 -0
  40. {ppgrid-0.2.2 → ppgrid-0.3.0}/data/melb_houses.csv +0 -0
  41. {ppgrid-0.2.2 → ppgrid-0.3.0}/examples/run_melb.py +0 -0
  42. {ppgrid-0.2.2 → ppgrid-0.3.0}/ppgrid/__init__.py +0 -0
  43. {ppgrid-0.2.2 → ppgrid-0.3.0}/ppgrid/__main__.py +0 -0
  44. {ppgrid-0.2.2 → ppgrid-0.3.0}/ppgrid/idwgrid.py +0 -0
  45. {ppgrid-0.2.2 → ppgrid-0.3.0}/tests/__init__.py +0 -0
  46. {ppgrid-0.2.2 → ppgrid-0.3.0}/tests/test_acceptance.py +0 -0
  47. {ppgrid-0.2.2 → ppgrid-0.3.0}/tests/test_ppgrid.py +0 -0
@@ -0,0 +1,22 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ # Runner scoping (2026-10-06, per Frank/jarate pattern): PRs run on
12
+ # GitHub-hosted runners; pushes to main run on the self-hosted 'nf'
13
+ # runners (2x, label 'nf', marzukia org). Inline expression — the
14
+ # env context is NOT available in runs-on (GitHub parse error).
15
+ runs-on: ${{ github.event_name == 'pull_request' && 'ubuntu-latest' || 'nf' }}
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - uses: astral-sh/setup-uv@v4
19
+ - run: uv sync --all-extras
20
+ - run: uv run --extra dev ruff check .
21
+ - run: uv run --extra dev ruff format --check .
22
+ - run: uv run pytest
ppgrid-0.3.0/AGENTS.md ADDED
@@ -0,0 +1,143 @@
1
+ # AGENTS.md
2
+
3
+ Working notes + rules for AI agents (and humans) touching this repo. Read this before editing.
4
+
5
+ ## What this is
6
+
7
+ `ppgrid` — pull-push mipmap scattered-data interpolation. Turns `N` geolocated points
8
+ into a pair of int16 GeoTIFFs (value percentile + support-km) at continent scale in
9
+ minutes, no GPU. It is an **IDW approximation**: fast, visually clean, and
10
+ deliberately *not* a spatially exact interpolator (the output is for visualisation,
11
+ not downstream calculation).
12
+
13
+ - Python 3.11+, `uv` managed. Entry point `ppgrid` (`ppgrid.pipeline:main`; `ppgrid.idwgrid` is a back-compat shim).
14
+ - Deps: numpy, pandas, pyproj, rasterio. Optional `[parquet]` (pyarrow).
15
+ - Tests: `uv run pytest` (93 tests). Lint/format: `ruff` (line-length 120).
16
+
17
+ ## Repo layout
18
+
19
+ - `ppgrid/pipeline.py` — the Pipeline + CLI. All the logic lives here. `ppgrid/idwgrid.py` is a thin back-compat shim (module alias to `ppgrid.pipeline`).
20
+ - `ppgrid/calibrate.py` — transform selection + spatially-blocked CV fill-cap.
21
+ - `data/` — example inputs: `melb_houses.csv` (13,580 pts), `all_equakes.csv` (44,376 pts).
22
+ - `examples/` — committed outputs + repro scripts + README images (see "Images" below).
23
+ - `tests/` — pytest. `examples/run_melb.py` regenerates the Melbourne example rasters.
24
+
25
+ ## Versioning / release
26
+
27
+ - `pyproject.toml` `version` is the source of truth; `__version__` reads it via
28
+ `importlib.metadata`. Keep `CHANGELOG.md` in step with every version bump.
29
+ - Releasing: bump `version` + `uv.lock`, update CHANGELOG, push a tag, the
30
+ `publish.yml` workflow uploads to PyPI. The README PyPI badge shows the *published*
31
+ version, so it lags the repo until a tag is cut.
32
+
33
+ ## Rules — do not fuck these up
34
+
35
+ ### Images in the README (this bit bites)
36
+ - The README is the PyPI `long_description`. **Every image MUST be an absolute
37
+ `https://raw.githubusercontent.com/marzukia/ppgrid/main/examples/...` URL.**
38
+ Relative `examples/...` paths render on GitHub but 404 on PyPI's static host.
39
+ - README images are committed under `examples/`. They resolve from `main`, so a
40
+ new image is invisible on the repo's README **until the branch merges to main**.
41
+ - The benchmark charts (`bench_headtohead.png`, `bench_scaling.png`) are generated
42
+ with **`charted`** (github.com/marzukia/charted — zero-dep Python SVG/PNG). Regenerate,
43
+ don't hand-edit:
44
+ ```bash
45
+ uvx --from "charted[png]" python # build BarChart / LineChart, .to_png_bytes()
46
+ ```
47
+ Data for those charts lives in the blog post
48
+ (`mrzk.io` ppgrid post, `fig-bar-benchmark.json` / `fig-line-scaling.json`).
49
+
50
+ ### The `--percentile-step` guard
51
+ - `pipeline.py` uses `if pct_step is not None:` (NOT `if pct_step:`). A step value of
52
+ `0` is invalid (validated to `(0, 100]`) but the `is not None` form is the correct
53
+ guard and matches the docstring. Do not "simplify" it back to a truthiness check.
54
+
55
+ ### CRS defaults
56
+ - Working CRS `EPSG:6933` (Wagner VII, equal-area, metres are true) for interpolation.
57
+ - Output CRS `EPSG:3857` (Web Mercator) for the final GeoTIFF.
58
+ - Input `EPSG:4326`. All three are CLI-overridable (`--work-crs`, `--out-crs`,
59
+ `--src-crs`). Don't hardcode a different default.
60
+
61
+ ### Output encoding is opinionated — leave it
62
+ - Value band = **percentile, not raw value**: `percentile = DN / scale`, default
63
+ scale 100 so DN 5000 = 50th percentile. int16 fits 0-10000. Raw values are restored
64
+ via the `calibration.json` LUT. This is deliberate (clean vectorisation/MVT, rounds
65
+ to whole numbers). If you "fix" it to store raw values you break the int16 design
66
+ and the decoding example in the README.
67
+
68
+ ### Data provenance
69
+ - `melb_houses.csv`: Kaggle Melbourne Housing Snapshot, **CC BY-NC-SA 4.0**.
70
+ - `all_equakes.csv`: USGS FDSN, **Public Domain**.
71
+ - Keep the Data Lineage table in the README accurate if the equake download window
72
+ or filters change.
73
+
74
+ ## Git / pushing
75
+
76
+ - The repo lives under the `marzukia` GitHub org. The `monkytheluffy` bot identity is
77
+ NOT a collaborator (push 403s). Push with the `marzukia` PAT:
78
+ ```bash
79
+ PAT=$(cat ~/.config/marzukia-pat)
80
+ git push "https://x-access-token:${PAT}@github.com/marzukia/ppgrid" <branch>
81
+ ```
82
+ - Branches go to a PR, not main, per the fleet merge gate.
83
+
84
+ ## What changed recently (2026-10-07, PR #39 review fixes: M-1..M-3, Y-1)
85
+
86
+ - M-1: equakes anchors regenerated under the rasterio 1.5.1 lock (the committed
87
+ files were cut on 1.5.0; the vendored GDAL emits different BigTIFF ZSTD tile
88
+ bytes — arrays equal, tile offsets moved). New shas in the A.6 table:
89
+ `examples/equakes/value.tif` `99c8a01c…`, `support_km.tif` `5e545742…`.
90
+ `bench/compare_tier2.py` re-run: bit-identical (melb anchors unchanged).
91
+ - M-2: `turbop.resolve_cap` clamps a preset against `min(physical, cgroup)`
92
+ (a capped slice OOMs at its own limit); the pipeline wires both reads via
93
+ `Pipeline._resolve_turbo_cap` (before: preset 32 on a 25.8 GB slice planned
94
+ 32 GB; now clamps to 17.8 GB).
95
+ - M-3: `turbop.per_box_peak_bytes` (64 B/cell bottom-up + 2 GB fixed, x
96
+ in-flight blocks + points/DN extra) + `precheck(per_box_peak_bytes=…)`;
97
+ the pipeline computes it from the worst snapped box after `grid()`.
98
+ Non-strict per-box over budget now warns "best-effort, OOM risk, output may
99
+ be partial" instead of promising exit-0 correct output; strict names the
100
+ per-box infeasibility. Verified: melb @1m --max-ram 8 warns 189.7 GB est >
101
+ 6.8 GB budget, then dies 137.
102
+ - Y-1: BigTIFF slot widths in the parallel ZSTD write. `_tif_parse_ifd` sizes
103
+ tags per TIFF type (type 16 LONG8 added; unknown type = hard error) and
104
+ returns `sz324/sz325/fmt324/fmt325`; `_turbo_write_parallel` patches 324
105
+ (8-byte slots) and 325 (4-byte slots) at their real strides, shared
106
+ `_TIFF_TYPE_SIZES` with the overlap guard. Pre-fix, one 8-byte stride
107
+ corrupted both arrays on every real BigTIFF output (7888 tiles: 325 sizes
108
+ read `2000, 0, 2001, 0, …`). Pinned: BigTIFF parallel byte-identity test +
109
+ IFD parse value asserts.
110
+ - Tests: 170 -> 176 (resolve_cap cgroup clamp, per-box peak model + gate,
111
+ pipeline wiring, BigTIFF parallel, IFD value pins).
112
+
113
+ ## What changed recently (2026-10-07, turbo write path + CLI, issue #33)
114
+
115
+ - CLI: `--turbo` (optional preset `16|32|64|128`; bare = auto cap), `--max-ram <GB>` (implies turbo), `--ram-gb` (alias of `--max-ram`), `--turbo-strict`. Pre-check after `grid()` prints cap/budget/regime used; shared infeasible (per-box regime) -> strict: stderr error + exit 3, else `[warn]` + continue on per-box with turbo warp/write (exit 0, correct output).
116
+ - `_write_rasters` turbo path: no work-CRS intermediate files. Quantised int16 DN fields stay in RAM (budget-gated: `nx*ny*4 > budget//2` -> file-based reproject path with MT warp) and are reprojected tile-by-tile through the same GDAL warp kernel as the serial path (`_reproject_band_array`; same 2048px tiles, same 512px serial flush, same metadata) -> byte-identical output, sha256-tested.
117
+ - Parallel per-tile ZSTD (`_turbo_write_parallel`): GDAL zero-filled reference head serialises the exact IFD/tag layout; the pipeline patches TileOffsets/TileByteCounts + oracle-verified zstd frames. Oracle mismatch (e.g. CPL zstd pfn vs libtiff streaming params differ on a stack) -> serial GDAL write, byte-exact.
118
+ - Budget-derived shared cap (S3.3): turbo mode derives the cells cap from `Bt = 0.85*C - 4 GB`; non-turbo keeps `_SHARED_MAX_CELLS = 2.5e8`; the `1e8` in-RAM/memmap tier is unchanged.
119
+ - `_prepare_shared` / `box_count_banded` run threaded under the budgeted worker count when turbo (`plan.workers`); `n_threads == 1` keeps the exact serial path (A8).
120
+ - `turbop._read_cgroup_max_gb` fixed: walks the process cgroup hierarchy to the root and takes the smallest finite `memory.max` (a slice limit above the service cgroup binds). Previously only the root cgroup was read, so a memory-capped user slice saw full physical RAM and auto `--turbo` could plan a regime that OOMs the slice.
121
+ - `examples/melb/10m/value.tif` regenerated: the committed anchor predated a pipeline change; current `main` and this branch produce the new bytes (verified against pre-turbo HEAD).
122
+ - Tests: `tests/test_turbo.py` now 22 (A9 regime pins, strict exit 3, non-strict continue, CLI flags/wiring, array-vs-file reproject sha, parallel-zstd byte-identity, 4GiB guard, IFD classic+BigTIFF parse, e2e in-RAM identity, budget-gate fallback, out==work CRS, `box_count_banded` MT, `test_cgroup_reader` walk-up).
123
+
124
+ ## What changed recently (2026-10-06, turbo read-side MT, 0.3.0)
125
+
126
+ - New `Pipeline(n_threads=...)` kwarg (default `1`, serial path unchanged, byte-identical output). NOT a CLI flag.
127
+ - `_reproject_band`: 2048px warp tiles per row band run on a `ThreadPoolExecutor` via the module-level `_warp_dst_tile` helper (per-tile source open: GDAL handles are not thread-safe). The 512px flush loop STAYS serial raster-scan (file bytes depend on tile order). Do not "parallelise" the flush.
128
+ - `ingest`: CSVs >= 100k rows per chunk split into complete-record byte ranges (`_csv_chunk_tasks`, newline scan + quote-parity guards, `None` on any doubt -> serial parse) and parsed in a `ProcessPoolExecutor` (`_read_csv_chunk`, explicit float64 dtypes + per-chunk dtype assert). Chunks concat in original row order (`bin_points` is order-sensitive); row-count mismatch re-runs the serial parse.
129
+ - rasterio's benign `NotGeoreferencedWarning` is ignored at import (CPython warning filters are process-global, not thread-safe; the library's nested suppression leaks under concurrent warps). A matching `filterwarnings` entry in `[tool.pytest.ini_options]` restates it for pytest.
130
+ - Tests: `tests/test_turbo.py` (6, sha256/bit-identity + fallbacks). Bench: `bench/turbo_bench.py`, `bench/turbo_decomp.py`.
131
+ - Design doc in-repo: `docs/turbo-design.md` (accepted criteria A4-A8).
132
+
133
+ ## What changed recently (2026-09-22, the "polish" pass)
134
+
135
+ - Bumped to **0.2.1**.
136
+ - `--percentile-step` flag (round output percentiles to a step, e.g. 5 → 90/95/100)
137
+ + the `is not None` guard fix.
138
+ - README: added hero image (`examples/melb_4panel.jpg`), replaced the old single
139
+ wall-time-vs-resolution chart with two `charted` benchmark charts (head-to-head vs
140
+ gdal at 100K pts + wall-time-vs-point-count scaling), added side-by-side/zoom
141
+ comparison images, fixed the clone URL (`pullpush` → `ppgrid`). Removed stale
142
+ `bench_combined/time/size.png`.
143
+ - All README image refs converted to absolute raw.githubusercontent URLs for PyPI.
@@ -0,0 +1,91 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [0.3.0] - 2026-10-08
8
+ - Full-AU (16M points, `--res 100`): 50:46 -> 2:33 wall time with sha256-identical outputs (marzuki-hydrogen, `--turbo 64`, regime A).
9
+ - Turbo: write path + budget cap + CLI (issue #33):
10
+ - `--turbo` (optional preset `16|32|64|128`; bare = auto `min(physical, cgroup) - 8 GB`), `--max-ram <GB>` (implies turbo), `--ram-gb` alias, `--turbo-strict`.
11
+ - Pre-check after `grid()`: prints the cap/budget/regime actually used; shared infeasible (per-box regime) -> strict: error + exit 3, else `[warn]` + continue on the per-box path with turbo warp/write (exit 0, correct output).
12
+ - `_write_rasters` turbo: skips the work-CRS intermediate files, keeps the quantised int16 DN fields in RAM, and reprojects tile-by-tile from arrays through the same GDAL warp kernel (byte-identical to the file-based path, sha256-tested). Optional parallel per-tile ZSTD via `zstdmt.compress_tiles`, gated by the compression oracle (mismatch -> serial GDAL write, byte-exact). In-RAM DN is budget-gated; oversized grids fall back to the file-based reproject path with MT warp.
13
+ - Budget-derived shared cap (S3.3) replaces the hard 2.5e8 cell cap in turbo mode; non-turbo keeps 2.5e8; the 1e8 in-RAM/memmap tier is unchanged.
14
+ - `_prepare_shared` / `box_count_banded` run threaded under the budgeted worker count when turbo (serial at `n_threads == 1`, A8 preserved).
15
+ - Fixed `turbop._read_cgroup_max_gb`: now walks the process cgroup hierarchy to the root and takes the smallest finite `memory.max` (a slice limit above the service cgroup binds; previously only the root cgroup was read, so capped user slices saw the full physical RAM and the auto cap could plan a regime that OOMs the slice).
16
+ - Regenerated stale anchor `examples/melb/10m/value.tif` (byte-identical to current `main` output; the committed file predated a pipeline change).
17
+ - RAM budget `Bt = 0.85 * C - 4 GB` (`C` = effective RAM cap) drives regime selection: A = full in-RAM shared, B = input in RAM + banded val/sup memmap, C = per-box warp/write with an 8 GB page-cache floor, per_box = below the 8 GB cap floor.
18
+ - Turbo: speed pass 3 — regime-A in-RAM shared path + byte-exact parallel writer (issue #39):
19
+ - Regime A keeps the shared full fields (val_full/sup_full) and the quantised DN fields (val_dn/sup_dn) on tmpfs memmaps (reclaimable as clean file cache) instead of anon RAM; `_pull_push_descent` gained optional `out_val/out_sup` and writes the final descent level straight into the shared fields (bit-exact in both the threaded and serial branches).
20
+ - Byte-exact parallel tile writer: full-thread-id TID scratch paths (low-16-bit id collisions truncated a live scratch file mid-run), tile frames reassembled into global 512 raster-scan (block-major assembly was wrong), reference head written as one 512^2 tile. sha256-identical to the committed serial anchors.
21
+ - Reproject: the source band is wrapped once in a no-copy `MemoryDataset` — rasterio's ndarray source form copied the whole band into a fresh MEM dataset per warp call (3 GB per 2048^2 block at full-AU; 8 in-flight copies OOM-killed the run).
22
+ - `_quantize` rewritten in-place (same IEEE ops/order, bit-identical) with per-thread f32+f64 scratch; the int16 outputs are owned per call (thread-local scratch raced the `ex.map` prefetch and corrupted ~10% of blocks).
23
+ - Fixed the support blend reusing the value `om` buffer across blends; `MADV_NOHUGEPAGE` on large fresh arrays (THP=always with sync defrag made first-touch faults 20-40 us/page).
24
+ - Turbo: pipeline read-side MT (issue #32) behind a new `Pipeline(n_threads=...)` kwarg (default `1`: the serial code path, byte-identical output):
25
+ - `_reproject_band`: the 2048px warp tiles of each row band run on a `ThreadPoolExecutor` (the GDAL warp runs under `nogil`; each tile opens its own source handle, GDAL dataset handles are not thread-safe). The 512px flush loop stays serial raster-scan, so the on-disk bytes never change (sha256-equal to the serial run is test-covered).
26
+ - `ingest`: CSVs with enough rows (>= 100k per chunk) are split into complete-record byte ranges (newline scan with quote-parity guards; any doubt falls back to the serial parse) and parsed in a `ProcessPoolExecutor` (`read_csv` holds the GIL, so threads cannot overlap the parse). Each chunk parses the wanted columns with explicit float64 dtypes and asserts per-chunk dtype equality; chunks concatenate in original row order (`bin_points` is order-sensitive). A row-count mismatch re-runs the serial parse.
27
+ - New `tests/test_turbo.py` (6 tests): MT reproject sha256-equality vs serial, MT ingest bit-identity vs the single whole-file parse on a 500k-row edge-row fixture (2**53 +/- 1, empty/inf rows, int-only column, quoted fields, non-adjacent columns), serial-parse fallbacks (small file, quoted embedded newline), end-to-end full-run sha256 equality `n_threads=1` vs `4`, and `n_threads` validation.
28
+ - Ignores rasterio's benign `NotGeoreferencedWarning` (MemoryDataset initial transform read): CPython warning filters are process-global and not thread-safe, so the library's nested suppression can leak under concurrent warps.
29
+ - Bench harnesses: `bench/turbo_bench.py` (full paths), `bench/turbo_decomp.py` (parallelisable part in isolation).
30
+
31
+ ## [0.2.2] - 2026-09-25
32
+ - Code hygiene (no behavior change; output rasters remain byte-identical) — issues #4/#5:
33
+ - Renamed `ppgrid/idwgrid.py` -> `ppgrid/pipeline.py`; `ppgrid/idwgrid` is now a thin backwards-compat shim, `ppgrid.Pipeline` is exported from the package root, and the `ppgrid` console script points at `ppgrid.pipeline:main`.
34
+ - Extracted the nested `_reproject_band` closure to a module-level function.
35
+ - Magic numbers in `pipeline.py`, `calibrate.py`, `pullpush.py` promoted to named constants (CRS defaults, int16 support-band encode/decode pair, tile size, memmap band indices, CLI defaults, float32 exact-integer limit, count epsilon, unresolved-support sentinel).
36
+ - Worker context: `_CTX` now holds the `_WorkerConfig` dataclass object (no per-field dict copy); adding a field is one edit.
37
+ - Deduplicated the 3x3-block neighbourhood / window / snapped-halo-box logic into shared helpers used by the per-block, shared-field, and test paths.
38
+ - Dropped the unused 4th point-memmap band.
39
+ - CLI: `--out` now defaults to `out/` (was `examples/`, which wrote build artifacts into the docs directory); argparse validation errors tested per flag.
40
+ - `ruff`: removed the `global-statement` ignore (no global statements left); COM812 remains ignored (pre-existing, conflicts with the formatter).
41
+ - Performance (issue #10): vectorized the output-band reproject warp; output byte-identical.
42
+ - Performance/correctness: shared full-field pyramid descent + fast box-count, calibrate hoisted out of the block loop; fixed `_CTX` not being cleared between runs; capped the shared path at 2.5e8 cells; stopped memmap descent at level `min(2, levels)` for levels=1 grids.
43
+ - Release/docs pass (issues #17/#19/#20/#22): slimmed the sdist (generated example rasters/images and the 8.3 MB `data/all_equakes.csv` no longer ship; ~87 MB -> <1 MB, `data/melb_houses.csv` still ships), added the ruff check + format gate to CI, documented the Python 3.11+ requirement in the README install section, fixed stale docs (test count, module names, closed PR ref), removed stray `examples/melb/value.tif.aux.xml`, untracked `.vscode/settings.json`, added `out/` to `.gitignore`.
44
+
45
+ ## [0.2.1] - 2026-09-22
46
+ - Added `--percentile-step` CLI flag: rounds output percentiles to the nearest step (e.g. `5` -> 90/95/100), clamped to 0-100. Recorded as a `percentile_step` tag on the value GeoTIFF.
47
+ - Fixed `percentile_step` guard: `if pct_step:` -> `if pct_step is not None:` so a falsy step is handled correctly and the validation is consistent with the docstring.
48
+ - README: added hero image (multi-resolution output), replaced the single wall-time-vs-resolution benchmark with a head-to-head vs `gdal_grid`/`gdal_rasterize` (100K points) and a wall-time-vs-point-count scaling chart (generated with `charted`), plus side-by-side comparison images. Fixed the source-clone URL (`pullpush` -> `ppgrid`).
49
+
50
+ ## [0.2.0] - 2026-08-08
51
+ - Added `__version__` to package.
52
+ - Moved `pyarrow` to optional `[parquet]` dependency.
53
+ - Added `CHANGELOG.md`.
54
+ - Added `SECURITY.md` and `SUPPORT.md`.
55
+ - Added `CODEOWNERS` and pre-commit config.
56
+ - Added `examples/run_melb.py` reproducible example script.
57
+ - Added CI and PyPI badges to README.
58
+ - Stated benchmark hardware in README.
59
+ - Fixed README project structure and CLI table.
60
+ - Added `python` keyword and Python 3.14 classifier.
61
+ - Fixed emdash in pyproject.toml description.
62
+ - Regenerated `uv.lock`.
63
+
64
+ ## [0.1.4] - 2026-08-08
65
+ - Fixed PyPI project URLs to point to the renamed GitHub repository (`ppgrid`).
66
+ - Added homepage link between PyPI and GitHub.
67
+
68
+ ## [0.1.3] - 2026-08-08
69
+ - Fixed error message tuple (trailing comma in `pullpush.py`).
70
+ - Removed dead `_init_worker` function.
71
+ - Added index validation to `bin_points`.
72
+ - Added 4 acceptance tests (blocked=whole, coverage, georef, validity).
73
+ - Fixed README grammar, install section, structure.
74
+ - Removed `.DS_Store` files from git.
75
+ - Fixed all ruff warnings and errors.
76
+
77
+ ## [0.1.2] - 2026-08-08
78
+ - Re-published after making repository public for README images.
79
+
80
+ ## [0.1.1] - 2026-08-08
81
+ - Fixed README image paths for PyPI rendering.
82
+
83
+ ## [0.1.0] - 2026-08-08
84
+ - Initial release of ppgrid.
85
+ - Fast continent-scale raster interpolation for scattered point data.
86
+ - IDW interpolation with pull-push mipmap pipeline.
87
+ - Windowed reprojection (no OOM on large datasets).
88
+ - CLI with validation and --version flag.
89
+ - Calibrate module for transform selection + cross-validation.
90
+ - 17 tests across 3 test files.
91
+ - Melbourne housing example (13.5k points at 6 resolutions).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ppgrid
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: Fast continent-scale raster interpolation for scattered point data using pull-push mipmap algorithms. Turns millions of geolocated points into gapless GeoTIFF in minutes. No GPU required.
5
5
  Project-URL: Homepage, https://github.com/marzukia/ppgrid
6
6
  Project-URL: Repository, https://github.com/marzukia/ppgrid
@@ -0,0 +1,69 @@
1
+ """Turbo read-side MT (issue #32): speed evidence for reproject warp + ingest.
2
+
3
+ Repro: uv run python -m bench.turbo_bench
4
+ """
5
+
6
+ import tempfile
7
+ import time
8
+ from pathlib import Path
9
+
10
+ import numpy as np
11
+ import pandas as pd
12
+ import rasterio
13
+ from rasterio.warp import calculate_default_transform
14
+
15
+ from ppgrid.pipeline import NODATA, TILE_PX, Pipeline, _reproject_band
16
+ from tests.test_turbo import _write_src_tif
17
+
18
+ TMP = Path(tempfile.mkdtemp(prefix="turbo_bench"))
19
+
20
+
21
+ def median(f, runs=3):
22
+ ts = []
23
+ for _ in range(runs):
24
+ t0 = time.perf_counter()
25
+ f()
26
+ ts.append(time.perf_counter() - t0)
27
+ return float(np.median(ts))
28
+
29
+
30
+ def bench_reproject():
31
+ src = TMP / "src.tif"
32
+ _write_src_tif(src, 5000, 4000)
33
+ with rasterio.open(src) as ds:
34
+ t, w, h = calculate_default_transform(ds.crs, "EPSG:3857", ds.width, ds.height, *ds.bounds)
35
+ profile = {
36
+ "driver": "GTiff",
37
+ "dtype": "int16",
38
+ "nodata": NODATA,
39
+ "compress": "ZSTD",
40
+ "tiled": True,
41
+ "blockxsize": TILE_PX,
42
+ "blockysize": TILE_PX,
43
+ "predictor": 2,
44
+ "count": 1,
45
+ }
46
+ out = TMP / "out.tif"
47
+ t1 = median(lambda: _reproject_band(str(src), str(out), profile, "EPSG:3857", t, w, h))
48
+ t4 = median(lambda: _reproject_band(str(src), str(out), profile, "EPSG:3857", t, w, h, n_threads=4))
49
+ print(f"reproject band {w}x{h}: serial {t1:.3f}s, 4t {t4:.3f}s, speedup {t1 / t4:.2f}x")
50
+
51
+
52
+ def bench_ingest(n_rows=1_000_000):
53
+ rng = np.random.default_rng(0)
54
+ csv = TMP / "big.csv"
55
+ pd.DataFrame(
56
+ {
57
+ "value": rng.normal(0.0, 100.0, n_rows),
58
+ "longitude": rng.uniform(110.0, 155.0, n_rows),
59
+ "latitude": rng.uniform(-50.0, -10.0, n_rows),
60
+ },
61
+ ).to_csv(csv, index=False)
62
+ t1 = median(lambda: Pipeline(str(csv), "value", "longitude", "latitude", str(TMP / "o1"), n_threads=1).ingest())
63
+ t4 = median(lambda: Pipeline(str(csv), "value", "longitude", "latitude", str(TMP / "o4"), n_threads=4).ingest())
64
+ print(f"ingest {n_rows} rows: serial {t1:.3f}s, 4t {t4:.3f}s, speedup {t1 / t4:.2f}x")
65
+
66
+
67
+ if __name__ == "__main__":
68
+ bench_reproject()
69
+ bench_ingest()
@@ -0,0 +1,96 @@
1
+ """Decompose turbo speedup: warp-only (GIL release) + parse-only (pool).
2
+
3
+ The full-path numbers in turbo_bench.py include the serial tail (the 512px
4
+ flush loop, pyproj transform) which Amdahl-caps the gain. This harness
5
+ measures the parallelisable part in isolation.
6
+
7
+ Repro: uv run python -m bench.turbo_decomp
8
+ """
9
+
10
+ import tempfile
11
+ import time
12
+ from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
13
+ from functools import partial
14
+ from pathlib import Path
15
+
16
+ import numpy as np
17
+ import pandas as pd
18
+ import rasterio
19
+ from rasterio.warp import calculate_default_transform
20
+
21
+ from ppgrid import pipeline as pm
22
+ from tests.test_turbo import _write_src_tif
23
+
24
+ TMP = Path(tempfile.mkdtemp(prefix="turbo_bench2"))
25
+
26
+
27
+ def median(f, runs=3):
28
+ ts = []
29
+ for _ in range(runs):
30
+ t0 = time.perf_counter()
31
+ f()
32
+ ts.append(time.perf_counter() - t0)
33
+ return float(np.median(ts))
34
+
35
+
36
+ def warp_only():
37
+ src = TMP / "src.tif"
38
+ _write_src_tif(src, 5000, 4000)
39
+ with rasterio.open(src) as ds:
40
+ t, w, h = calculate_default_transform(ds.crs, "EPSG:3857", ds.width, ds.height, *ds.bounds)
41
+
42
+ def warp_all(n_threads):
43
+ warp_tile = 2048
44
+ for j0 in range(0, h, warp_tile):
45
+ band_h = min(warp_tile, h - j0)
46
+ i0s = list(range(0, w, warp_tile))
47
+ p = partial(
48
+ pm._warp_dst_tile,
49
+ str(src),
50
+ j0=j0,
51
+ band_h=band_h,
52
+ warp_tile=warp_tile,
53
+ dst_width=w,
54
+ dst_transform=t,
55
+ dst_crs="EPSG:3857",
56
+ )
57
+ if n_threads > 1:
58
+ with ThreadPoolExecutor(max_workers=n_threads) as ex:
59
+ for _ in ex.map(p, i0s):
60
+ pass
61
+ else:
62
+ for i0 in i0s:
63
+ p(i0)
64
+
65
+ t1 = median(lambda: warp_all(1))
66
+ t4 = median(lambda: warp_all(4))
67
+ print(f"warp-only {w}x{h}: serial {t1:.3f}s, 4t {t4:.3f}s, speedup {t1 / t4:.2f}x")
68
+
69
+
70
+ def parse_only(n_rows=1_000_000):
71
+ rng = np.random.default_rng(0)
72
+ csv = TMP / "big.csv"
73
+ pd.DataFrame(
74
+ {
75
+ "value": rng.normal(0.0, 100.0, n_rows),
76
+ "longitude": rng.uniform(110.0, 155.0, n_rows),
77
+ "latitude": rng.uniform(-50.0, -10.0, n_rows),
78
+ },
79
+ ).to_csv(csv, index=False)
80
+ t1 = median(lambda: pd.read_csv(csv))
81
+
82
+ wanted = ["value", "longitude", "latitude"]
83
+ tasks, _ = pm._csv_chunk_tasks(str(csv), wanted, 4)
84
+
85
+ def p4():
86
+ with ProcessPoolExecutor(max_workers=4) as ex:
87
+ chunks = list(ex.map(pm._read_csv_chunk, tasks))
88
+ return np.concatenate([c[0] for c in chunks])
89
+
90
+ t4 = median(p4)
91
+ print(f"parse-only {n_rows} rows: serial {t1:.3f}s, 4t {t4:.3f}s, speedup {t1 / t4:.2f}x")
92
+
93
+
94
+ if __name__ == "__main__":
95
+ warp_only()
96
+ parse_only()