SHARPpy Reimagined - Modern sounding analysis and SHARPpy-style rendering for Python 3.11+. SHARPpy Reimagined is a modernized, standalone fork of SHARPpy, focused on packageable Python 3.11+ workflows, Qt6/PySide6 rendering, and reproducible point-sounding tools. It keeps the familiar SPC-style skew-T, hodograph, hazard, and derived-parameter views while adding clean command-line entry points, bundled resources, and a test-backed decoder/extractor layer.
README
SHARPpy Reimagined
Modern sounding analysis and SHARPpy-style rendering for Python 3.11+.
SHARPpy Reimagined is a modernized, standalone fork of
SHARPpy, focused on packageable Python
3.11+ workflows, Qt6/PySide6 rendering, and reproducible point-sounding tools.
It keeps the familiar SPC-style skew-T, hodograph, hazard, and derived-parameter
views while adding clean command-line entry points, bundled resources, and a
test-backed decoder/extractor layer.
Highlights
Headless PNG rendering for .npz, SPC tabular, BUFKIT, PECAN, and WRF-ARW
text sounding inputs.
Portable .npz point-sounding output from UWyo, the independent IEM RAOB
archive, ERA5, WRF-ARW, Herbie-backed forecast models, and ECCC GeoMet.
Resumable multi-point/multi-hour jobs with one download per shared model hour,
bounded concurrency, atomic outputs, and a checksummed versioned manifest.
Qt6/PySide6 compatibility shims around the upstream SHARPpy widget stack.
A supported Rust-primary numerical and point-decoding backend, with an
independently optimized Python fallback and equivalence coverage across all
13 configured public forecast models.
A complete inverted/light sounding palette shared by the interactive GUI and
headless renderer, including contrast-aware labels and derived displays.
Offline UWyo station catalog plus package-relative bundled fonts.
Property-based pytest coverage for decoders, derived parameters, hazards,
renderer-facing widgets, and extraction paths.
The upstream SHARPpy==1.4.0a5 package is installed with --no-deps because
its published metadata pins an old NumPy version. SHARPpy Reimagined provides
the modern runtime dependencies separately.
Rust-primary backend
Official v0.4 Windows executables bundle the supported sharpmod_rs extension,
and the default auto mode uses Rust after validating its package version,
backend API, and required operations. The independently optimized Python
implementation remains a fully functional portable fallback, so source and
Python-only installations do not require Rust, Cargo, maturin, or a native
extension to run.
To add the Rust backend to a source installation, first install a stable Rust
toolchain (Rust 1.88 or newer), then run these commands in the same Python
environment as sharpmod:
sharpmod-rust-sync rebuilds only when sharpmod_rs is missing or its version
does not match this checkout, then verifies forced-Rust selection in a fresh
Python process. --check is non-mutating; use --force after editing native
source. Extension developers can still run maturin develop --release --locked
directly from rust/sharpmod-rs.
Select the backend with SHARPMOD_BACKEND before starting the application:
Value
Behavior
auto
Default. Use Rust when it loads; otherwise use Python and record the fallback reason.
python
Require the optimized Python implementation.
rust
Require the Rust extension; report an error if it is unavailable or cannot load.
Check the resolved backend without running a sounding workflow:
Rust is the supported primary backend when a compatible extension is present;
official v0.4 Windows binaries include it. Standalone native wheels are still
CI/build artifacts rather than a separately published Python package. See the
Rust backend guide for source-build instructions,
fallback behavior, platform status, limitations, tests, and benchmarks.
Forecast-decoder performance and validation
Version 0.4.1 includes the two independently optimized GRIB implementations
introduced in v0.4.0. The Python backend reuses a file inventory and
nearest-point selection, reads only the required scalar fields, and keeps
bounded inventory, point-selection, and decoded-sounding caches. The Rust
backend memory-maps each local subset,
iterates ecCodes messages without copying the GRIB payload, and returns one
NumPy-compatible matrix through a single Python call. Neither implementation
requires speculative parallel decoding.
Both decoders understand GRIB multi-field messages in which one physical
message contains separate U- and V-wind fields. Their equivalence preflight
checks selected grid coordinates, pressure ordering, missing masks, and values
within field-appropriate floating-point tolerances. The matrix covers HRRR,
RAP, NAM, NAM 3 km, HRW WRF-ARW, HRW FV3, RRFS-A, GFS, AIGFS, CFS, ECMWF IFS,
ECMWF-AIFS, and GEFS.
Products without a published relative- or absolute-vorticity field can retain
the full xarray compatibility path in the production extractor so the
neighbor-wind vorticity estimate is preserved; their direct point decoder is
still measured separately and is not an end-to-end timing of that production
route. Optional fields published at a single pressure,
such as GEFS omega, are aligned only to that pressure and remain missing at
other levels instead of being broadcast through the sounding. Optimized Python
and Rust return matching pressure-aligned omega values and missing masks. The
old/new GEFS benchmark difference records the correction of the frozen legacy
xarray full-column broadcast; it is not a Rust availability gap.
See the all-model benchmark table,
its raw JSON record,
and the benchmark methodology. Network transfer is
excluded from decoder timings, and the JSON retains fixture hashes, raw
samples, selected coordinates, build fingerprints, and equivalence results.
Desktop GUI
An interactive, legacy-SHARPpy-style desktop app is included:
sharpmod-gui # or: python -m sharpmod.gui
On Windows, source-checkout GUI runs use Python 3.11-3.13. If this command is
invoked by Python 3.14 and the checkout has a .venv or .gribenv, the launcher
automatically hands the GUI to that compatible environment before Qt starts.
The packaged Windows release already bundles Python 3.11.
The Sounding Picker opens with five ways to load a sounding:
Station Map — a clickable map of every UWyo radiosonde station over a
coastline basemap. Click a dot to select, double-click to open; scroll to
zoom, drag to pan, and pick a region from the Map area menu. Observation
times are selectable every three hours from 00Z through 21Z.
Station List — the full catalogue with live id/name filtering and the
same three-hourly UTC observation-time choices.
Forecast Model — click a point or enter latitude/longitude, then choose a
public model, UTC run, forecast hour, and optional ensemble member. The picker
checks that inventory in the background. If publication is delayed, it offers
the newest available earlier cycle without silently changing the selection;
an uncertain check never disables manual Fetch. The fetch runs in the
background and opens the point sounding in the SPC window. Timeline…
queues a selected range of as many as 72 hours into one viewer with a
slider, playback, step, and loop controls; completed hours remain available
after cancellation or a missing hour.
Reanalysis (ERA5) — choose any global point and hourly UTC analysis. The
picker previews the snapped 0.25-degree grid point, validates the optional
packages/CDS profile, caches completed point-hours, and keeps Qt responsive
while the synchronous CDS request runs in a worker.
Open File — a local .npz, SPC, BUFKIT, PECAN, or WRF-ARW text sounding
(or just drag the file onto the window). Its Raw WRF wrfout workflow
inspects a NetCDF domain/times in the background, validates a map point
against the actual curvilinear grid perimeter, then extracts and opens it.
Each sounding opens in the full interactive SPC window (the upstream SHARPpy
widget stack), so every interaction from the
SHARPpy GUI guide
works:
Right-click the Skew-T for the readout cursor, Modify Surface, parcel
lifting, and reset.
Click + drag temperature / dewpoint / wind points to edit the profile —
every index recalculates live.
Mouse wheel zooms; right-click the hodograph re-centers it, and
double-clicking the RM/LM markers sets the storm motion.
Double-click the lower-left inset to swap lifted parcels.
Keys: ← / → step in time, ↑ / ↓ change ensemble member, Space swaps
focus, I interpolates, C collects observed, W returns to the picker.
Undo / Redo:Ctrl+Z reverses profile, interpolation, and storm-motion
edits; Ctrl+Y reapplies them. Each viewer retains the latest 50 edits.
Data → Source & Quality Inspector… shows the provider/source route,
backend and decoder, cache status, level and missing-field counts, surface
vorticity provenance, and non-mutating QC warnings for the focused profile.
File → Preferences switches the color palette (Standard / Inverted /
Protanopia), units, and the parcel visualized by default when a Skew-T opens.
The picker also provides File → Downloaded Data Library… to browse,
validate, reopen/re-extract, pin, delete, or copy provenance for cached model
data. Locations → Manage Saved Locations… stores searchable named points,
supports versioned JSON import/export, and displays saved and recent points as
map markers. Only labels and coordinates are persisted.
GUI choices persist across launches, including temperature/wind/PWAT units,
palette, top/bottom readouts, default parcel, multi-sounding behavior, dismissed
tips, recent files, and last selections. On Windows they are stored in
%APPDATA%\SHARPpy Reimagined\settings.ini; set SHARPMOD_SETTINGS_PATH to
use a different INI file.
The Inverted palette is a complete light theme. Applying it updates the
Skew-T, hodograph, locator, storm slinky, inset products, IndexBoard, and
Streamwiseness panels in one live configuration change. Theme-dependent text,
rules, legends, and semantic annotations are contrast-adjusted for a light
canvas while plotted scientific values, units, and established dark-theme
colors remain unchanged. Headless rendering uses the same selected palette.
Analysis sessions
Use File → Save Analysis Session… (Ctrl+Shift+E) in a sounding window to
save every loaded sounding, the active profile, current profile/interpolation/
storm-motion edits, parcel selection, and viewer state. Open Analysis
Session… (Ctrl+Shift+O) is available from both the picker and sounding
window and restores the saved soundings together in one viewer.
Session files use the .sharpmod-session extension and a versioned, portable
JSON format; they do not execute code or embed source GRIB downloads. Forecast
download directories still follow the normal lifecycle and are deleted when
their original viewer closes.
Export
The sounding window's Export menu saves the current view:
Export Image (HD PNG) (Ctrl+E) — a 2x high-density image of the full
window, including the mounted derived-parameter panels, with a sensible
default filename (STATION_YYYYMMDDHHZ_hd.png) in your Desktop folder.
The result is dist/SHARPpy-Reimagined/SHARPpy-Reimagined.exe. Set
SHARPMOD_ONEFILE=1 in the build environment for a single self-extracting
dist/SHARPpy-Reimagined.exe instead.
The official v0.4 release workflow builds and installs sharpmod_rs before
PyInstaller packages the executable, making Rust the auto backend in the
published application. For custom local builds, the spec collects a compatible
installed extension when present; otherwise it logs a warning and produces a
fully functional Python-fallback bundle.
Command Line Tools
Command
Purpose
sharpmod-render
Render a sounding file to a PNG
uwyo-sounding
List, search, and fetch University of Wyoming soundings
observed-sounding
Fetch from UWyo with an explicit IEM RAOB fallback
era5-extract
Extract an ERA5 point sounding to .npz
model-extract
Fetch all pressure levels for a supported forecast-model point sounding
model-batch-extract
Run a resumable multi-point/multi-hour model job
wrf-extract
Extract a WRF-ARW point sounding to .npz
sharpmod-rust-sync
Check, rebuild when needed, and verify the local Rust backend
Forecast-model extraction (model-extract)
Install the GRIB stack before fetching model data. Add the render stack and the
upstream SHARPpy runtime when --render is needed:
Discover the installed CLI and check remote inventory before a large fetch:
model-extract --help
model-extract --list
model-extract gfs --probe --fxx 0
# Also download and open the pressure-level subset during the probe
model-extract gfs --probe --fxx 0 --open-subset
Fetch a point sounding by model key, latitude, and longitude:
# Keep the portable .npz and its .json metadata sidecar
model-extract gfs 35.18 -97.44 gfs_oun.npz --fxx 0 --loc "Norman, OK"
# Select an exact UTC cycle and forecast hour
model-extract gfs 35.18 -97.44 gfs_oun_f006.npz --run "2026-07-14 00:00" --fxx 6
# Render to a named PNG; fetched GRIB/.npz/.json data is removed afterward
model-extract hrrr 35.18 -97.44 --fxx 0 --render hrrr_oun.png
# Omit the PNG name to use the generated point-sounding filename stem
model-extract hrrr 35.18 -97.44 --fxx 0 --render
# Select an ensemble member (GEFS defaults to c00)
model-extract gefs 35.18 -97.44 gefs_p01.npz --fxx 0 --member p01
If --run is omitted, the CLI chooses the most recent configured cycle at or
before the current UTC time; upstream publication can lag that cycle, so use
--probe or pass an earlier --run when inventory is not available. Without
--render, the .npz and .json outputs remain. With --render, only the PNG
remains. The GUI instead retains fetched files until the sounding window closes.
Download acceleration and cache
The extractor keeps every pressure level published by the selected model while
avoiding fields that are duplicates for sounding construction. It tries the
smallest compatible route first:
HRRR F000 analyses use direct point reads from the public HRRR Zarr archive
and normalize those columns straight into the compact decoder contract;
Canadian GDPS/RDPS use ECCC GeoMet point queries with bounded layer fan-out.
Indexed subsets at or below 32 MiB use validated, coalesced HTTP byte ranges
after selecting a healthy equivalent provider. Every indexed model uses up
to four bounded range workers by default. Large coalesced spans are split
into balanced fragments, with one session per worker, pinned object
identity, ordered atomic assembly, resumable fragments, and an automatic
sequential-range fallback. Live
RRFS and
all-model transport/decode
records retain timing and byte-equivalence evidence for that default.
Larger HRRR, RAP, NAM, NAM 3 km, HRW WRF-ARW/FV3, GFS, CFS, and GEFS
transfers use a small NOAA NOMADS geographic subset; other indexed products
retain the range route.
Any unavailable or incompatible optimization falls back automatically to
the standard Herbie download path.
Local GRIB files decode directly into compact NumPy columns. Products without
a pressure-level vorticity field use a four-neighbor U/V stencil read directly
from two GRIB messages instead of opening xarray wind cubes. Multi-point batch
jobs vectorize both the sounding columns and those stencils, so each selected
message is unpacked once for all requested points; this is vectorized I/O, not
unsafe decoder threading.
The GUI keeps downloaded model hours under
%LOCALAPPDATA%\sharpmod\model-cache on Windows (or the platform cache folder),
up to 3 GB and 48 hours by default. In the File menu, Prefetch Next Forecast
Hour optionally warms the next valid hour, Clear Downloaded Model Cache
removes retained entries, and the model tab's Cancel button stops the active
request. Verified partial files from compatible range downloads are retained so
the same request can resume.
Advanced overrides are available for testing or constrained environments:
Environment variable
Default
Effect
SHARPMOD_HRRR_BACKEND
auto
auto, zarr, or grib for HRRR F000
SHARPMOD_POINT_BACKENDS
auto
Set to grib to bypass point/subregion routes
SHARPMOD_GRIB_DECODER
auto
auto uses direct point decoding with xarray fallback; direct requires it; xarray forces the compatibility path
SHARPMOD_PROVIDER_RACING
1
Set to 0 to disable equivalent-provider probes
SHARPMOD_RANGE_WORKERS
4
HTTP range-request workers for indexed models, clamped to 1-8; decoder execution remains serial
SHARPMOD_GEOMET_WORKERS
4
Concurrent ECCC GeoMet layer-point requests, clamped to 1-8
SHARPMOD_MODEL_CACHE
platform cache
Override the GUI model-cache directory
SHARPMOD_MODEL_CACHE_GB
3
Maximum retained cache size in GiB
SHARPMOD_MODEL_CACHE_HOURS
48
Maximum retained entry age
Batch and multi-point extraction
model-batch-extract accepts heterogeneous points and forecast hours. Requests
with the same model, UTC run, forecast hour, and member share one decoded
model-hour lease, while different hours run with a bounded 1-4 worker pool.
Single-point hours retain the normal point/subregion route and its GUI-compatible
spatial cache key; multi-point hours fetch one reusable field subset and decode
all local-GRIB points with vector element reads.
Every .npz and .json sidecar is atomic. The checksummed manifest is also
atomic, so rerunning the same command validates and skips completed outputs.
The Python API is sharpmod.batch_extract.run_batch(...); it accepts ordered
BatchRequest values and returns ordered per-request results plus completed
NPZ paths. Call BatchExtractor.cancel() for cooperative cancellation.
Pass an existing ModelHourCache as model_hour_cache= when a GUI or service
owns a longer-lived cache; the batch runner leases it but does not clear it.
Configured models
These are the canonical keys accepted by this checkout. model-extract --list
is the runtime source of truth and also reports known models that are not
enabled. Remote run availability still depends on the upstream provider.
Canonical key
Model / product
Coverage
Configured forecast hours
Aliases / notes
hrrr
HRRR pressure levels
CONUS
00/06/12/18Z: F000-F048 hourly; other cycles: F000-F018 hourly
—
rap
RAP 13 km AWIPS pressure levels
CONUS
F000-F051 hourly
—
nam
NAM 12 km pressure levels
CONUS
F000-F084 every 3 hours
—
nam-3km-conus
NAM 3 km CONUS nest
CONUS
F000-F060 hourly
nam3, nam-3km
hrw-wrf-arw
NOAA HiResW WRF-ARW 5 km
CONUS
F000-F048 hourly
hiresw-arw, hrw-arw
hrw-fv3
NOAA HiResW FV3 5 km
CONUS
F000-F048 hourly
hiresw-fv3
rrfs-a
RRFS-A 3 km pressure levels
CONUS
00/06/12/18Z: F000-F084 hourly; other cycles: F000-F018 hourly
rrfs
rrfs-a-alaska
RRFS-A 3 km pressure levels
Alaska
00/06/12/18Z: F000-F084 hourly; other cycles: F000-F018 hourly
rrfs-ak, rrfs-alaska
rrfs-a-hawaii
RRFS-A 2.5 km pressure levels
Hawaii
00/06/12/18Z: F000-F084 hourly; other cycles: F000-F018 hourly
rrfs-hi, rrfs-hawaii
rrfs-a-puerto-rico
RRFS-A 2.5 km pressure levels
Puerto Rico
00/06/12/18Z: F000-F084 hourly; other cycles: F000-F018 hourly
rrfs-pr, rrfs-puerto-rico
gfs
GFS 0.25-degree pressure levels
Global
F000-F120 hourly, then every 3 hours to F384
—
aigfs
AI-GFS pressure levels
Global
F000-F384 every 6 hours
Humidity is read from specific humidity
cfs
CFS 6-hourly pressure levels
Global
F000-F384 every 6 hours
Member 1 by default
ecmwf-ifs
ECMWF IFS Open Data
Global
F000-F144 every 3 hours, then every 6 hours to F360
ecmwf, ifs
ecmwf-aifs
ECMWF-AIFS Open Data
Global
F000-F144 every 3 hours, then every 6 hours to F360
aifs
gefs
GEFS 0.5-degree pressure levels
Global
F000-F384 every 3 hours
Control member c00 by default
gdps
Canadian GDPS 15 km point profile
Global
F000-F240 every 3 hours
00/12Z; gem-global, cmc-global
rdps
Canadian RDPS 10 km point profile
North America / Arctic
F000-F084 hourly
00/06/12/18Z; gem-regional, cmc-regional
# Observed sounding: try UWyo, then the independent IEM RAOB archive
observed-sounding fetch 72357 "2024-05-20 00" --out oun.npz --render oun.png
# Render the mixed-layer parcel on the Skew-T (MU is the default)
sharpmod-render oun.npz oun_ml.png --parcel ML
# Reanalysis / local WRF point soundings
era5-extract "2024-05-20 00:00" 35.18 -97.44 era5.npz --render
wrf-extract wrfout_d01_2024-05-20_00:00:00 35.18 -97.44 wrf.npz --render
# Canadian point sounding through ECCC GeoMet (no full-grid download)
model-extract gdps 45.50 -73.60 montreal.npz --run "2026-07-22 00" --fxx 6
era5-extract retrieves all 37 pressure levels from the official Copernicus
Climate Data Store API. Create a free CDS account, accept the ERA5 dataset
licence, and copy the credentials shown on the
CDS API setup page into
$HOME/.cdsapirc before the first request. Public forecast models continue to
use Herbie and do not require CDS credentials.
sharpmod-render --parcel accepts SFC, ML, FCST, MU, EFF, and
USER. Parcel keys are case-insensitive.
For the full setup reference, see installation.txt. For
usage recipes and Python API examples, see docs/USAGE.md.
Rust-backend setup, fallback behavior, and limitations are documented in
docs/RUST_BACKEND.md.
Data Flow
UWyo / ERA5 / WRF / public forecast models
|
v
portable .npz point sounding
|
v
sharpmod-render
|
v
SPC-style skew-T + hodograph PNG
Repository Map
sharpmod/
gui.py interactive desktop app (sounding picker + SPC window)
render.py headless PNG render entry point
backends/ optimized Python/Rust kernels and direct GRIB point decoders
sharptab/ derived-parameter and meteorological calculations
io/ decoders for SPC, BUFKIT, PECAN, WRF-ARW, .npz, and UWyo
viz/ Qt6/PySide6 rendering widgets
tools/ UWyo, ERA5, forecast-model, WRF, basemap, and render CLI tools
resources/ bundled fonts, station catalog, and GUI basemap/icons
tests/ unit, smoke, and property-based tests
packaging/
sharpmod_gui.spec PyInstaller spec for the standalone GUI build
rust/sharpmod-rs/ supported PyO3/maturin backend extension crate
benchmarks/ Python-versus-Rust equivalence-first timing harness
examples/
example_sounding.png
soundings/ bundled sample inputs
docs/
USAGE.md workflow guide and API examples
RUST_BACKEND.md Rust-primary setup, fallback behavior, and limitations
Attribution
This project builds on the abandoned upstream
SHARPpy project. See LICENSE
for license terms and attribution.