Metadata-Version: 2.4
Name: ksmesopy
Version: 0.7.3
Summary: Python API for the Kansas Mesonet
License: MIT
Project-URL: Homepage, https://mesonet.k-state.edu
Project-URL: Repository, https://github.com/soilwater/ksmesopy
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: numpy>=1.24
Requires-Dist: pandas>=2.0
Requires-Dist: matplotlib>=3.7
Provides-Extra: app
Requires-Dist: guile<2,>=1.0.0; extra == "app"

# ksmesopy

Python package for downloading and processing data from the [Kansas Mesonet](https://mesonet.k-state.edu), an environmental monitoring network operated by Kansas State University.

## Installation

```bash
pip install ksmesopy
pip install "ksmesopy[app]"  # Use this to also install a GUI
```

Dependencies: `numpy`, `pandas`, `matplotlib`. The desktop app additionally requires `guile>=1.0.0,<2`.

## Quick start

```python
import ksmesopy as ms

# Download daily temperature and precipitation for one station
df = ms.request_data(
    station="Manhattan",
    start="2024-01-01",
    end="2024-12-31",
    interval="day",
    variables=["TEMP2MAVG", "TEMP2MMIN", "TEMP2MMAX", "PRECIP"],
    verbose=True,   # print progress while downloading (default False)
)

# Optional: rename columns to snake_case
df = ms.rename_columns(df)
# TIMESTAMP → timestamp, TEMP2MAVG → tair_2m_avg, PRECIP → precip, …

# Snapshot: every station at a single time step
snap = ms.request_snapshot(
    timestamp="2025-01-01",   # for 5min/hour, include a time e.g. "2025-01-01 14:30"
    interval="day",
    variables=["TEMP2MAVG", "PRECIP"],
)
# TIMESTAMP, STATION, TEMP2MAVG, PRECIP — one row per station
```

## Desktop app

Run this in your terminal
```bash
ksmesopy-app
```

![Graphical User Interface](app_demo.gif)

A GUI for selecting stations, date ranges, variables, and intervals. Use the left navigation rail to switch between Inputs, Table, and Chart. Exports data to CSV and charts as PNG. The app ships with the Python package and runs directly without compilation; you can also launch it with `python -m ksmesopy.app`.

---

## API reference

### Stations

| Function | Returns | Description |
|---|---|---|
| `get_stations()` | `DataFrame` | Full station metadata table |
| `get_stations(names_only=True)` | `list[str]` | Sorted list of station names only |
| `get_stations_active()` | `DataFrame` | Availability table: `STATION`, `OBS_INTERVAL` (s), `START`, `END` |

### Data retrieval

| Function | Returns | Description |
|---|---|---|
| `request_data(station, start, end, interval, variables, *, verbose, sleep)` | `DataFrame` | Download data for one station. `interval` is `"day"`, `"hour"`, or `"5min"`. Daily timestamps are corrected from the Mesonet's next-day convention. Pass `verbose=True` to print progress (see [Progress reporting](#notes)). |
| `request_data_multi(stations, start, end, interval, variables, *, verbose, sleep)` | `dict[str, DataFrame]` | Same as above for a list of stations; returns one DataFrame per station. `verbose=True` adds a `[n/total] station` header before each station. |
| `request_snapshot(timestamp, interval, variables, *, verbose)` | `DataFrame` | One timestamp across **all** stations at once (the Mesonet `stn=all` endpoint). `interval` is `"day"`, `"hour"`, or `"5min"`. Returns one row per station with a `STATION` column. For `"day"`, the time component is ignored and the timestamp is corrected from the next-day convention, so results line up with `request_data()` for the same date. |
| `list_variables(interval=None)` | `list[dict]` | Variable catalogue filtered by interval, or all variables if `None`. Each entry has keys `api_name`, `snake_name`, `description`, `intervals`. |
| `rename_columns(df, preset="snake")` | `DataFrame` | Rename API column names to snake_case (e.g. `TEMP2MAVG` → `tair_2m_avg`). Pass a `dict` for a custom mapping. |

### Soil processing

| Function | Returns | Description |
|---|---|---|
| `calibrate_vwc(df, vwc_cols=None)` | `DataFrame` | Replace firmware VWC values with the KSU site-specific calibration. Requires `SOILKA*CM` and `SOILEC*CM` columns. Works on any subset of depths. |
| `compute_soil_water_storage(df)` | `DataFrame` | Trapezoidal soil water storage in the top 50 cm (mm). Requires all four VWC depths; adds a `STORAGE_MM` column. Call `calibrate_vwc()` first for calibrated storage. |

### Derived variables

| Function | Returns | Description |
|---|---|---|
| `growing_degree_days(tmin, tmax, base=10.0, ceiling=30.0)` | `ndarray` | Daily GDD. Both tmin/tmax clipped to `[base, ceiling]` before averaging. |
| `heat_index(temp, rh)` | `ndarray` | NOAA/NWS apparent temperature (°C). |
| `wind_chill(temp, wspd)` | `ndarray` | NWS wind chill (°C); valid for temp ≤ 10 °C, wind ≥ 1.3 m s⁻¹. |
| `temperature_humidity_index(temp, rh)` | `ndarray` | THI for livestock heat stress. Thresholds (dairy): <68 none · 68–72 mild · 72–80 moderate · 80–90 severe · >90 dangerous. |

### Reference evapotranspiration

| Function | Returns | Description |
|---|---|---|
| `reference_et_penman_monteith(doy, lat, elev, tmin, tmax, srad, wspd, rhmin=None, rhmax=None, *, vpd=None, wind_height=2.0)` | `ndarray` | FAO-56 Penman-Monteith. Supply humidity as either `rhmin=`+`rhmax=` or `vpd=` (at least one is required); when only `vpd=` is given, `ea` for net longwave is derived as `es − vpd`. `srad` in W m⁻², converted internally. `wind_height` (m) adjusts wind to 2 m via the FAO-56 log profile — pass `10.0` for the 10 m sensor. |
| `reference_et_hargreaves(doy, lat, tmin, tmax, *, tmean)` | `ndarray` | Hargreaves–Samani. Temperature only — no humidity, radiation, or wind needed. |

Both return ETo (mm day⁻¹) as an `ndarray`. Use `extraterrestrial_radiation(doy, lat)` if you also need Ra.

### Atmospheric helpers

| Function | Output |
|---|---|
| `saturation_vapor_pressure(temp)` | es (kPa) |
| `actual_vapor_pressure(temp, rh)` | ea (kPa) |
| `vapor_pressure_deficit(temp, rh)` | VPD (kPa, ≥ 0) |
| `slope_saturation_vapor_pressure(temp)` | Δ (kPa °C⁻¹) |
| `atmospheric_pressure(elev)` | P (kPa) |
| `psychrometric_constant(elev)` | γ (kPa °C⁻¹) |
| `extraterrestrial_radiation(doy, lat)` | Ra (MJ m⁻² day⁻¹) |
| `net_radiation(srad_mj, tmin, tmax, ea, elev, doy, lat)` | Rn (MJ m⁻² day⁻¹) |
| `srad_to_mj(srad, period)` | energy (MJ m⁻²) |

All functions accept scalars or NumPy arrays.

### Charts

Each function draws onto a Matplotlib `Axes` supplied by the caller, so panels compose freely inside any figure layout. All functions accept API column names (`TEMP2MAVG`) or snake_case names (`tair_2m_avg`) interchangeably, and return the axes they drew on so they can be further customised.

```python
import matplotlib.pyplot as plt
import ksmesopy as ms

fig, axes = plt.subplots(5, 1, sharex=True, figsize=(12, 14))

ms.plot_temperature(axes[0], df, ["TEMP2MAVG", "TEMP2MMIN", "TEMP2MMAX"])
ms.plot_precip(axes[1], df, "PRECIP")
ms.plot_humidity(axes[2], df, "RELHUM2MAVG")
ms.plot_solar_radiation(axes[3], df, ["SRAVG", "Ra"])  # Ra drawn dashed
ms.plot_vwc(axes[4], df)                               # auto-detects VWC columns

plt.tight_layout()
plt.savefig("meteogram.png", dpi=150)
```

All functions accept API column names (`TEMP2MAVG`) or snake_case names (`tair_2m_avg`) interchangeably, and return the axes they drew on so they can be further customised.

| Function | Key behaviour |
|---|---|
| `plot_temperature(ax, df, variables, *, band, ylabel, legend)` | Shaded band when min/avg/max triplet detected (`band=True`); plain lines otherwise. Works for air and soil temperature. |
| `plot_precip(ax, df, variable, *, ylabel, color)` | Bar chart, bar width inferred from timestamp spacing. |
| `plot_humidity(ax, df, variables, *, ylabel, legend)` | Lines, y-axis fixed 0–100 %. |
| `plot_vpd(ax, df, variables, *, ylabel, legend)` | Filled area + line, y-axis starts at 0. |
| `plot_solar_radiation(ax, df, variables, *, ylabel, legend)` | Filled area for observed; dashed line for Ra columns (detected by name). |
| `plot_wind(ax, df, speed, direction, *, ylabel, legend)` | Speed as line; direction overlaid as scatter on a twin y-axis with N/E/S/W ticks. |
| `plot_vwc(ax, df, variables, *, ylabel, legend)` | Sequential colormap shallow→deep; auto-detects VWC columns if `variables=None`. |
| `plot_et(ax, df, variables, *, bar, ylabel, legend)` | Line by default; `bar=True` for daily totals. |

---

## Variable catalogue

Intervals: **D** = daily only · **H** = hourly and daily · **A** = 5-min, hourly, and daily

### Atmospheric

| API name | snake_case | Description | Unit | Intervals |
|---|---|---|---|---|
| `TEMP2MAVG` | `tair_2m_avg` | Air temperature 2 m avg | °C | A |
| `TEMP2MMIN` | `tair_2m_min` | Air temperature 2 m min | °C | D |
| `TEMP2MMAX` | `tair_2m_max` | Air temperature 2 m max | °C | D |
| `TEMP10MAVG` | `tair_10m_avg` | Air temperature 10 m avg | °C | A |
| `TEMP10MMIN` | `tair_10m_min` | Air temperature 10 m min | °C | D |
| `TEMP10MMAX` | `tair_10m_max` | Air temperature 10 m max | °C | D |
| `RELHUM2MAVG` | `rh_2m_avg` | Relative humidity 2 m avg | % | A |
| `RELHUM2MMIN` | `rh_2m_min` | Relative humidity 2 m min | % | D |
| `RELHUM2MMAX` | `rh_2m_max` | Relative humidity 2 m max | % | D |
| `VPDEFAVG` | `vpd_avg` | Vapor pressure deficit avg | kPa | A |
| `PRESSUREAVG` | `pressure_avg` | Atmospheric pressure avg | kPa | A |
| `PRECIP` | `precip` | Precipitation gauge 1 | mm | A |
| `PRECIP2` | `precip2` | Precipitation gauge 2 | mm | A |
| `SRAVG` | `srad` | Solar radiation avg | W m⁻² | A |
| `WSPD2MAVG` | `wspd_2m_avg` | Wind speed 2 m avg | m s⁻¹ | A |
| `WSPD2MMAX` | `wspd_2m_max` | Wind speed 2 m max | m s⁻¹ | H¹ |
| `WDIR2M` | `wdir_2m` | Wind direction 2 m | ° | A |
| `WDIR2MSTD` | `wdir_2m_std` | Wind direction 2 m std dev | ° | A |
| `WSPD10MAVG` | `wspd_10m_avg` | Wind speed 10 m avg | m s⁻¹ | A |
| `WSPD10MMAX` | `wspd_10m_max` | Wind speed 10 m max | m s⁻¹ | H¹ |
| `WDIR10M` | `wdir_10m` | Wind direction 10 m | ° | A |
| `WDIR10MSTD` | `wdir_10m_std` | Wind direction 10 m std dev | ° | A |

¹ Available at 5-min and daily only (not hourly).

### Soil temperature — dedicated probes

| API name | snake_case | Description | Unit | Intervals |
|---|---|---|---|---|
| `SOILTMP5AVG` | `tsoil_5cm` | Soil temperature 5 cm avg | °C | A |
| `SOILTMP5MIN` | `tsoil_5cm_min` | Soil temperature 5 cm min | °C | D |
| `SOILTMP5MAX` | `tsoil_5cm_max` | Soil temperature 5 cm max | °C | D |
| `SOILTMP10AVG` | `tsoil_10cm` | Soil temperature 10 cm avg | °C | A |
| `SOILTMP10MIN` | `tsoil_10cm_min` | Soil temperature 10 cm min | °C | D |
| `SOILTMP10MAX` | `tsoil_10cm_max` | Soil temperature 10 cm max | °C | D |

### Soil — CS655 sensors

| API name | snake_case | Description | Unit | Intervals |
|---|---|---|---|---|
| `SOILTMP5AVG655` | `tsoil_5cm_655` | Soil temperature 5 cm | °C | A |
| `SOILTMP10AVG655` | `tsoil_10cm_655` | Soil temperature 10 cm | °C | A |
| `SOILTMP20AVG655` | `tsoil_20cm_655` | Soil temperature 20 cm | °C | A |
| `SOILTMP50AVG655` | `tsoil_50cm_655` | Soil temperature 50 cm | °C | A |
| `SOILKA5CM` | `ka_5cm` | Dielectric constant 5 cm | — | A |
| `SOILKA10CM` | `ka_10cm` | Dielectric constant 10 cm | — | A |
| `SOILKA20CM` | `ka_20cm` | Dielectric constant 20 cm | — | A |
| `SOILKA50CM` | `ka_50cm` | Dielectric constant 50 cm | — | A |
| `SOILEC5CM` | `ec_5cm` | Electrical conductivity 5 cm | dS m⁻¹ | A |
| `SOILEC10CM` | `ec_10cm` | Electrical conductivity 10 cm | dS m⁻¹ | A |
| `SOILEC20CM` | `ec_20cm` | Electrical conductivity 20 cm | dS m⁻¹ | A |
| `SOILEC50CM` | `ec_50cm` | Electrical conductivity 50 cm | dS m⁻¹ | A |
| `VWC5CM` | `vwc_5cm` | Volumetric water content 5 cm | m³ m⁻³ | A |
| `VWC10CM` | `vwc_10cm` | Volumetric water content 10 cm | m³ m⁻³ | A |
| `VWC20CM` | `vwc_20cm` | Volumetric water content 20 cm | m³ m⁻³ | A |
| `VWC50CM` | `vwc_50cm` | Volumetric water content 50 cm | m³ m⁻³ | A |

> **VWC note:** the Mesonet API returns VWC computed by the CS655 firmware equation. Call `calibrate_vwc()` to replace those values with the KSU site-specific calibration. Requires `SOILKA*CM` and `SOILEC*CM` to be fetched alongside `VWC*CM`. Works for any subset of depths independently.

---

## Notes

**Precipitation.** The Mesonet operates dual tipping-bucket rain gauges at most stations. `request_data()` returns both (`PRECIP`, `PRECIP2`); the desktop app merges them to the row-wise maximum automatically. For scripted use, merge manually:

```python
df["PRECIP"] = df[["PRECIP", "PRECIP2"]].max(axis=1)
df.drop(columns="PRECIP2", inplace=True)
```

**Daily timestamps.** The Mesonet API stores each day's aggregated values at 00:00 of the following calendar day. `request_data()` corrects for this automatically; the returned `TIMESTAMP` always reflects the observation date.

**Network snapshots.** `request_snapshot()` fetches one time step for every station in a single call — handy for mapping a variable across Kansas on a given day or 5-minute mark. It's restricted to a single instant by design; for a time series at one station use `request_data()`. Daily snapshots use the same next-day correction as `request_data()`, so the two agree for any given date.

**Progress reporting.** Long requests are split into chunks of 3 000 records, so a multi-year download is several round trips to the API. Pass `verbose=True` to `request_data()`, `request_data_multi()`, or `request_snapshot()` to print one line per request as it is issued:

```python
df = ms.request_data("Manhattan", "2013-01-01", "2024-12-31", "day",
                     ["TEMP2MAVG", "PRECIP"], verbose=True)
```

```
Manhattan: 2013-01-01 - 2021-03-20
Manhattan: 2021-03-21 - 2024-12-31
Manhattan: done — 4383 rows × 2 variables
```

Dates carry a time component for `"hour"` and `"5min"` intervals. Failed attempts are reported too, so a slow download that is quietly retrying is visible rather than looking hung. `verbose` defaults to `False`, keeping scripted use silent.

Progress messages are also sent to the `ksmesopy` logger at `DEBUG` level regardless of `verbose`, so an application can capture them instead of printing:

```python
import logging
logging.basicConfig(level=logging.DEBUG)
logging.getLogger("ksmesopy").setLevel(logging.DEBUG)
```

**Missing values.** The API encodes missing observations as `"M"`. These are converted to `NaN` on read. Periods before a station or sensor was installed are pre-filled with `NaN` rather than omitted.

---

## License

MIT
