diff --git a/.gitignore b/.gitignore index 9f11b75..5ef4674 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .idea/ +_build/.DS_Store \ No newline at end of file diff --git a/_build/.doctrees/README.doctree b/_build/.doctrees/README.doctree deleted file mode 100644 index 098f02a..0000000 Binary files a/_build/.doctrees/README.doctree and /dev/null differ diff --git a/_build/.doctrees/ag-forecasting-api.doctree b/_build/.doctrees/ag-forecasting-api.doctree deleted file mode 100644 index 1cf61ad..0000000 Binary files a/_build/.doctrees/ag-forecasting-api.doctree and /dev/null differ diff --git a/_build/.doctrees/blog/index.doctree b/_build/.doctrees/blog/index.doctree index 8f5d094..e78e2f9 100644 Binary files a/_build/.doctrees/blog/index.doctree and b/_build/.doctrees/blog/index.doctree differ diff --git a/_build/.doctrees/blog/post/2026-05-18-ag-forecasting-case-study.doctree b/_build/.doctrees/blog/post/2026-05-18-ag-forecasting-case-study.doctree index 8cde8b9..90491a9 100644 Binary files a/_build/.doctrees/blog/post/2026-05-18-ag-forecasting-case-study.doctree and b/_build/.doctrees/blog/post/2026-05-18-ag-forecasting-case-study.doctree differ diff --git a/_build/.doctrees/environment.pickle b/_build/.doctrees/environment.pickle index b44285e..22c1954 100644 Binary files a/_build/.doctrees/environment.pickle and b/_build/.doctrees/environment.pickle differ diff --git a/_build/.doctrees/worker.doctree b/_build/.doctrees/worker.doctree index 9b6fe6f..597ba68 100644 Binary files a/_build/.doctrees/worker.doctree and b/_build/.doctrees/worker.doctree differ diff --git a/_build/html/.buildinfo b/_build/html/.buildinfo index a4fb4ae..bd233c7 100644 --- a/_build/html/.buildinfo +++ b/_build/html/.buildinfo @@ -1,4 +1,4 @@ # Sphinx build info version 1 # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. -config: fe0d8d89ecad092c9e1a52c98b324dc8 +config: 41c69e2ccac39ed8464828d3817fb491 tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/_build/html/README.html b/_build/html/README.html deleted file mode 100644 index 796377e..0000000 --- a/_build/html/README.html +++ /dev/null @@ -1,510 +0,0 @@ - - - - - - - - - - - OpenLambda GitHub Pages — MyST Markdown Site — OpenLambda - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - -
-
-
-
-
- -
- -
- - - - - -
-
- - - - - -
- - - - - - - - - - - - - -
- -
- - - -
- -
-
- -
-
- -
- -
- -
- - -
- -
- -
- - - - - - - - - - - - - - - - - - - -
- -
- -
-
- - - -
-

OpenLambda GitHub Pages — MyST Markdown Site

- -
- -
-
- - - - -
-
-

OpenLambda GitHub Pages — MyST Markdown Site#

-

This repo deploys automatically to GitHub Pages using Jupyter Book -with MyST Markdown as the technical documentation of Open Lambda.

-
-
-

File Structure#

-
.
-├── index.md                        ← Homepage content (MyST Markdown)
-├── _config.yml                     ← Jupyter Book site settings
-├── _toc.yml                        ← Table of contents (add more pages here)
-├── requirements.txt                ← Python deps for the build
-├── .gitignore
-└── .github/
-    └── workflows/
-        └── deploy.yml              ← GitHub Actions auto-deploy workflow
-
-
-
-
-
-

One-Time Setup (do this once per repo)#

-
-

1. Clone the repo and copy these files in#

-
git clone https://github.com/open-lambda/open-lambda.github.io.git
-cd open-lambda.github.io
-
-# Copy all files from this bundle into the repo root, then:
-git add .
-git commit -m "Add MyST Markdown Jupyter Book site"
-git push
-
-
-
-
-
-

Local Preview#

-
pip install -r requirements.txt
-jupyter-book build .
-# Open in browser:
-open _build/html/index.html
-
-
-
-
- - - -
- -
- -
- - - - - - -
- -
-
-
- -
- - - - - - -
- - - -
-
-
- - - - - - - - \ No newline at end of file diff --git a/_build/html/_sources/README.md b/_build/html/_sources/README.md deleted file mode 100644 index 42a0660..0000000 --- a/_build/html/_sources/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# OpenLambda GitHub Pages — MyST Markdown Site - -This repo deploys automatically to GitHub Pages using [Jupyter Book](https://jupyterbook.org) -with [MyST Markdown](https://myst-parser.readthedocs.io/) as the technical documentation of Open Lambda. - ---- - -## File Structure - -``` -. -├── index.md ← Homepage content (MyST Markdown) -├── _config.yml ← Jupyter Book site settings -├── _toc.yml ← Table of contents (add more pages here) -├── requirements.txt ← Python deps for the build -├── .gitignore -└── .github/ - └── workflows/ - └── deploy.yml ← GitHub Actions auto-deploy workflow -``` - ---- - -## One-Time Setup (do this once per repo) - -### 1. Clone the repo and copy these files in - -```bash -git clone https://github.com/open-lambda/open-lambda.github.io.git -cd open-lambda.github.io - -# Copy all files from this bundle into the repo root, then: -git add . -git commit -m "Add MyST Markdown Jupyter Book site" -git push -``` - -## Local Preview - -```bash -pip install -r requirements.txt -jupyter-book build . -# Open in browser: -open _build/html/index.html -``` diff --git a/_build/html/_sources/ag-forecasting-api.md b/_build/html/_sources/ag-forecasting-api.md deleted file mode 100644 index 2a2e377..0000000 --- a/_build/html/_sources/ag-forecasting-api.md +++ /dev/null @@ -1,932 +0,0 @@ -# Agricultural Forecasting API - Technical Documentation - -A FastAPI-based service for crop disease forecasting and biomass prediction using weather data integration from multiple sources. Developed by the University of Wisconsin-Madison Data Science Institute. - ---- - -## Table of Contents - -1. [Overview](#overview) -2. [Architecture](#architecture) -3. [Technology Stack](#technology-stack) -4. [API Endpoints](#api-endpoints) -5. [Core Features](#core-features) -6. [Data Flow](#data-flow) -7. [Disease Models](#disease-models) -8. [Biomass Models](#biomass-models) -9. [Weather Data Sources](#weather-data-sources) -10. [Response Format](#response-format) -11. [Development Setup](#development-setup) -12. [Deployment](#deployment) - ---- - -## Overview - -The Agricultural Forecasting API provides real-time crop disease risk predictions and winter rye biomass estimates for Wisconsin agricultural locations. The system integrates historical weather data with scientifically-validated disease forecasting models to help farmers and agricultural professionals make informed management decisions. - -**Key Capabilities:** -- Real-time disease risk predictions for corn and soybean -- Winter rye biomass accumulation forecasts -- Multi-source weather data integration (IBM EIS, WiscoNet, NOAA) -- Geospatial queries by coordinates or weather station ID -- Batch processing for multi-station analysis -- GeoJSON-formatted responses for mapping integration - ---- - -## Architecture - -### High-Level Design - -``` -┌─────────────────────────────────────────────────────────────┐ -│ FastAPI Application │ -├─────────────────────────────────────────────────────────────┤ -│ │ -│ ┌─────────────────┐ ┌──────────────────┐ ┌─────────────┐ │ -│ │ /v2/ibm │ │ /v2/wisconet_g │ │ /v2/models │ │ -│ │ (Coordinates) │ │ (Station-based) │ │ (Metadata) │ │ -│ └────────┬────────┘ └────────┬─────────┘ └─────────────┘ │ -│ │ │ │ -├───────────┼────────────────────┼─────────────────────────────┤ -│ │ │ │ -│ ┌────────▼────────────┐ ┌───▼──────────────────┐ │ -│ │ IBM Service │ │ Pipeline / WiscoNet │ │ -│ │ (ibm_service.py) │ │ (pipeline.py) │ │ -│ │ │ │ │ │ -│ │ • Authenticate │ │ • Load stations │ │ -│ │ • Fetch raw data │ │ • Fetch measurements │ │ -│ │ • Build hourly │ │ • Compute biomass │ │ -│ │ • Build daily │ │ • Annotate risks │ │ -│ │ • Compute rolling │ │ │ │ -│ │ averages │ │ │ │ -│ └────────┬────────────┘ └───┬──────────────────┘ │ -│ │ │ │ -├───────────┼───────────────────┼──────────────────────────────┤ -│ │ │ │ -│ ┌────────▼─────────────────────┴──────┐ │ -│ │ Risk Models & Biomass Calc │ │ -│ │ (api/models/risk_models.py) │ │ -│ │ │ │ -│ │ • Tarspot risk (corn) │ │ -│ │ • Gray Leaf Spot (corn) │ │ -│ │ • Frogeye Leaf Spot (soybean) │ │ -│ │ • White Mold (soybean) │ │ -│ │ • Winter Rye Biomass (logistic) │ │ -│ └────────┬─────────────────────────────┘ │ -│ │ │ -├───────────┼──────────────────────────────────────────────────┤ -│ │ │ -│ ┌────────▼────────────────────────────┐ │ -│ │ Output Formatting & Response │ │ -│ │ (adapters/geojson_adapter.py) │ │ -│ │ │ │ -│ │ • GeoJSON Feature Collections │ │ -│ │ • Time-series grouping │ │ -│ │ • Station metadata embedding │ │ -│ └──────────────────────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────┘ -``` - -### Directory Structure - -``` -ag_forecasting_api/ -├── app.py # Main FastAPI entry point (v2) -├── app_v1.py # Legacy API (deprecated, v1) -├── requirements.txt # Python dependencies -├── Dockerfile # Container image definition -├── docker-compose.yml # Multi-container orchestration -│ -├── api/ # Core business logic -│ ├── __init__.py -│ ├── config/ -│ │ └── constants.py # Configuration, URLs, thresholds -│ │ -│ ├── models/ -│ │ ├── risk_models.py # Disease & biomass calculations -│ │ └── disease_metadata.py # Model metadata registry -│ │ -│ ├── services/ -│ │ ├── ibm_service.py # IBM EIS weather integration -│ │ ├── wisconet_service.py # WiscoNet station pipeline -│ │ └── risk_processor.py # Risk annotation orchestration -│ │ -│ ├── routes/ -│ │ ├── wisconet.py # /v2/ag_models_wrappers/wisconet_g -│ │ └── models.py # /v2/ag_models_wrappers/models -│ │ -│ ├── adapters/ -│ │ └── geojson_adapter.py # DataFrame → GeoJSON converter -│ │ -│ ├── schemas/ -│ │ ├── geojson_schema.py # Response schema definitions -│ │ └── model_schema.py # Model metadata schemas -│ │ -│ ├── utils/ -│ │ ├── math_helpers.py # GDD, logistic, rolling averages -│ │ ├── conversions.py # Unit conversions (F↔C, mph↔m/s) -│ │ └── caching.py # Station & measurement caching -│ │ -│ └── pipeline.py # Orchestrates WiscoNet workflow -│ -├── api_cache/ # Cached API responses (JSON) -│ ├── metadata_stations_*.json -│ └── station_data_*.json -│ -└── materials/ - └── example_callapi.ipynb # Usage examples -``` - -### Module Responsibilities - -| Module | Purpose | Key Functions | -|--------|---------|---------------| -| `ibm_service.py` | IBM weather API integration | `get_weather_with_risk()`, `_fetch_raw_hourly()`, `_build_daily()`, `_annotate_winter_rye_biomass()` | -| `wisconet_service.py` | WiscoNet station data pipeline | `_compute_winter_rye_biomass()`, station loading & filtering | -| `risk_models.py` | Disease & biomass calculations | `calculate_tarspot_risk()`, `calculate_winter_rye_biomass()`, etc. | -| `pipeline.py` | Async WiscoNet orchestration | `retrieve()`, multi-station batch processing | -| `math_helpers.py` | Shared mathematical utilities | `gdd_sine()`, `logistic()`, `rolling_mean()` | -| `geojson_adapter.py` | Response formatting | `dataframe_to_featurecollection()` | - ---- - -## Technology Stack - -### Core Dependencies - -| Component | Library | Version | Purpose | -|-----------|---------|---------|---------| -| Web Framework | FastAPI | ^0.100 | REST API framework | -| ASGI Server | Uvicorn | ^0.24 | ASGI server | -| Data Processing | Pandas | ^2.0 | Tabular data manipulation | -| Numeric Computing | NumPy | ^1.24 | Numerical calculations | -| Data Validation | Pydantic | ^2.0 | Request/response schema validation | -| HTTP Client | requests | ^2.31 | Synchronous HTTP calls | -| Async HTTP | aiohttp | ^3.9 | Async HTTP requests | -| Logging | Python logging | stdlib | Structured logging | - -### External Services - -- **IBM Environmental Intelligence Suite (EIS)**: Premium weather API with historical-on-demand (HOD) data -- **WiscoNet**: Public Wisconsin mesonet weather station network -- **NOAA**: National weather service data for historical context - -### Deployment - -- **Container**: Docker with Alpine Linux base -- **Orchestration**: Docker Compose for multi-service deployments -- **Reverse Proxy**: Traefik for URL routing and TLS termination -- **Health Monitoring**: Periodic curl-based health checks - ---- - -## API Endpoints - -### V2 Endpoints (Current) - -#### Disease Risk & Biomass by Coordinates (IBM) - -```http -GET /v2/ag_models_wrappers/ibm -``` - -Query parameters: -- `forecasting_date` (required): Reference date (YYYY-MM-DD) -- `latitude` (required): Decimal degrees -- `longitude` (required): Decimal degrees -- `planting_date` (optional): For biomass calculation (YYYY-MM-DD) -- `termination_date` (optional): For WiscoNet biomass integration (YYYY-MM-DD) -- `API_KEY` (required): IBM EIS credential -- `TENANT_ID` (required): IBM EIS credential -- `ORG_ID` (required): IBM EIS credential - -Example: -```bash -curl "http://localhost:8000/v2/ag_models_wrappers/ibm?forecasting_date=2025-05-15&latitude=43.0&longitude=-89.0&planting_date=2024-09-15&API_KEY=xxx&TENANT_ID=yyy&ORG_ID=zzz" -``` - -Response: GeoJSON FeatureCollection with hourly and daily weather data plus risk scores. - ---- - -#### Disease Risk & Biomass by WiscoNet Stations - -```http -GET /v2/ag_models_wrappers/wisconet_g -``` - -Query parameters: -- `forecasting_date` (required): Reference date (YYYY-MM-DD) -- `risk_days` (optional): Days to compute rolling risk (default: 1) -- `station_id` (optional): Filter to single station (e.g., "ALTN") -- `planting_date` (optional): For winter rye biomass (YYYY-MM-DD) -- `termination_date` (optional): For crop termination biomass (YYYY-MM-DD) -- `disease` (optional): Filter to specific disease model - -Example: -```bash -curl "http://localhost:8000/v2/ag_models_wrappers/wisconet_g?forecasting_date=2025-05-15&station_id=ALTN&planting_date=2024-09-15" -``` - -Response: GeoJSON FeatureCollection grouped by station with time-series risk data. - ---- - -#### Model Metadata - -```http -GET /v2/ag_models_wrappers/models -``` - -Returns list of all available disease models with descriptions, crops, risk categories, and input requirements. - ---- - -### Legacy V1 Endpoints - -The API maintains backward compatibility with v1 endpoints (mounted at `/v1`), including: -- `/v1/sites` - Station listing -- `/v1/predictions` - Disease predictions -- `/v1/models` - Model metadata - ---- - -## Core Features - -### 1. Multi-Source Weather Integration - -The API combines weather data from multiple sources with intelligent fallback: - -``` -┌──────────────────────────────────────────────────┐ -│ Weather Data Request (lat/lng, date range) │ -└────────────────┬─────────────────────────────────┘ - │ - ┌───────────┴──────────────┐ - │ │ - ▼ ▼ -┌─────────────────┐ ┌──────────────────┐ -│ IBM EIS API │ │ WiscoNet Network │ -│ (Premium, │ │ (Public, │ -│ high res) │ │ station-based) │ -└────────┬────────┘ └────────┬─────────┘ - │ │ - └────────────┬───────────┘ - │ - ▼ - ┌──────────────────────┐ - │ Data Harmonization │ - │ • Timezone aware │ - │ • Unit conversion │ - │ • Interpolation │ - └──────────┬───────────┘ - │ - ▼ - ┌──────────────────────┐ - │ Growing Degree Days │ - │ • Sine method │ - │ • Base: 0°C, Cap: 35°C│ - └──────────┬───────────┘ - │ - ▼ - ┌──────────────────────┐ - │ Rolling Averages │ - │ • 7, 14, 21, 30-day │ - └──────────┬───────────┘ - │ - ▼ - ┌──────────────────────┐ - │ Risk Model Input │ - │ (Ready for scoring) │ - └──────────────────────┘ -``` - -### 2. Efficient Data Caching - -Reduces redundant API calls through tiered caching: - -```python -# Cache layers (in api/utils/caching.py) -- Station metadata: 7-day TTL -- Measurements: 6-hour TTL -- IBM responses: Variable TTL based on forecast date -- File system: JSON cache in api_cache/ directory -``` - -### 3. Async Pipeline Processing - -Multi-station workflows use async processing for performance: - -```python -# pipeline.py -async def _run_pipeline(...): - # 1. Load all active stations (filtered) - stations = load_stations_sync(...) - - # 2. Fetch all measurements concurrently - measurements = await fetch_all_measurements_async(stations) - - # 3. Merge metadata (station location, timezone, etc.) - merged = merge_station_metadata(measurements, stations) - - # 4. Compute winter rye biomass (if planting_date provided) - if planting_date: - biomass = _compute_winter_rye_biomass(merged, planting_date) - merged = merge_biomass(merged, biomass) - - # 5. Optionally fetch WiscoNet-specific biomass data - if both planting_date and termination_date: - wisconet_biomass = await fetch_wisconet_biomass(...) - merged = merge_wisconet_biomass(merged, wisconet_biomass) - - # 6. Compute disease risks in parallel - risk_df = compute_risks_in_parallel( - merged, num_workers=4 - ) - - return risk_df -``` - -### 4. Intelligent Date & Timezone Handling - -- Automatic timezone conversion to station local time -- Date arithmetic aware of DST transitions -- Support for both absolute dates and relative forecasts - ---- - -## Disease Models - -### Tarspot Risk (Corn) - -**Crop:** Corn -**Disease:** *Phyllachora maydis* (Tar Spot fungus) - -**Model Type:** Ensemble logistic regression (two sub-models averaged) - -**Inputs:** -- 30-day mean air temperature (°C) -- 30-day max relative humidity (%) -- 14-day mean nighttime hours with RH ≥ 90% - -**Output:** Risk probability (0–1), classified as: -- **Inactive** (mean temp < 10°C) -- **1.Low** (prob < 0.20) -- **2.Moderate** (prob 0.20–0.35) -- **3.High** (prob > 0.35) - -**Reference:** Damon et al. - ---- - -### Gray Leaf Spot Risk (Corn) - -**Crop:** Corn -**Disease:** *Cercospora zeae-maydis* (Gray Leaf Spot) - -**Model Type:** Single logistic regression - -**Inputs:** -- 21-day min air temperature (°C) -- 30-day min dew point (°C) - -**Assumptions:** Growth stage V10–R3, no irrigation - -**Output:** Risk probability (0–1), same classification as Tarspot - ---- - -### Frogeye Leaf Spot Risk (Soybean) - -**Crop:** Soybean -**Disease:** *Cercospora sojina* (Frogeye Leaf Spot) - -**Model Type:** Logistic regression - -**Inputs:** -- Degree-days above 14°C (cumulative from emergence) -- Rainfall in previous 14 days (mm) -- Hours with RH ≥ 85% (14-day rolling) - -**Output:** Risk probability (0–1) - ---- - -### White Mold Risk (Soybean) - -**Crop:** Soybean -**Disease:** *Sclerotinia sclerotiorum* (White Mold / Sclerotinia) - -**Model Type:** Dual variant (irrigated & non-irrigated) - -**Inputs:** -- Days since planting -- Cumulative precipitation (mm) -- Soil moisture availability - -**Variants:** -- **Dry:** Non-irrigated fields -- **Irrigated 15":** 15-inch row spacing -- **Irrigated 30":** 30-inch row spacing - -**Output:** Apothecial presence probability (0–1) - ---- - -### Winter Rye Biomass (NEW) - -**Crop:** Winter Rye -**Metric:** Aboveground dry biomass (lb/acre) - -**Model Type:** Logistic growth model - -**Formula:** - -$$\text{logit} = b_0 + b_{pd} \cdot \text{plant\_doy} + b_{pf} \cdot \text{precip\_fall}$$ - -$$\text{pred} = \frac{\text{logit}}{1 + e^{-k(\text{gdd\_total}-x_0)}}$$ - -$$\text{biomass} = \max(0, \text{pred}^2)$$ - -**Coefficients:** -- $b_0 = 423.1$ (intercept) -- $b_{pd} = -1.031$ (day-of-year effect) -- $b_{pf} = -0.2878$ (fall precipitation effect) -- $k = 0.003663$ (logistic rate) -- $x_0 = 1049.0$ (inflection point GDD) - -**Inputs:** -- Planting day-of-year (1–366) -- Precipitation during fall establishment (mm) -- Cumulative GDD from planting to current date (0°C base) - -**Output:** -- **Biomass** (lb/acre): Numeric prediction -- **Biomass Color:** - - **Gray**: 0–2000 lb/acre (low coverage) - - **Yellow**: 2000–4500 lb/acre (moderate) - - **Green**: >4500 lb/acre (high/dense) -- **Biomass Message**: Human-readable interpretation - -**Use Cases:** -- Cover crop management planning -- Soil health assessment -- Erosion mitigation evaluation -- Residue management decisions - ---- - -## Biomass Models - -### Winter Rye Biomass Calculation Pipeline - -```python -def _annotate_winter_rye_biomass( - rye_daily: pd.DataFrame, - planting_date: str -) -> pd.DataFrame: - """ - Compute winter rye biomass from daily weather data. - - Steps: - 1. Parse planting_date → day-of-year - 2. Filter to dates >= planting_date - 3. Compute daily GDD (sine method, 0°C base, 35°C cap) - 4. Accumulate GDD and precipitation - 5. Call calculate_winter_rye_biomass() for each row - 6. Merge results back into DataFrame - """ - - # Step 1: Parse planting date - plant_date_obj = datetime.fromisoformat(planting_date).date() - plant_doy = plant_date_obj.timetuple().tm_yday - - # Step 2: Filter to >= planting date - rye_daily = rye_daily[rye_daily['date'] >= planting_date].copy() - - # Step 3-4: GDD accumulation - rye_daily['plant_doy'] = plant_doy - rye_daily['precip_daily'] = rye_daily['precip1Hour_sum'].fillna(0) - rye_daily['gdd_0c'] = rye_daily.apply( - lambda row: gdd_sine(row['temperature_min'], row['temperature_max']), - axis=1 - ) - rye_daily['gdd_total'] = rye_daily['gdd_0c'].cumsum() - - # Compute fall precipitation (Sep 1 to planting) - fall_start = plant_date_obj.replace(month=9, day=1) - fall_precip = rye_daily[ - (rye_daily['date'] >= str(fall_start)) & - (rye_daily['date'] < planting_date) - ]['precip_daily'].sum() - - # Step 5: Model calculation - biomass_results = rye_daily.apply( - lambda row: calculate_winter_rye_biomass( - plant_doy=row['plant_doy'], - precip_fall=fall_precip, - gdd_total=row['gdd_total'] - ), - axis=1, - result_type='expand' - ) - - # Step 6: Merge results - for col in ['biomass_lb_acre', 'biomass_color', 'biomass_message']: - rye_daily[col] = biomass_results[col] - - return rye_daily -``` - ---- - -## Weather Data Sources - -### IBM Environmental Intelligence Suite (EIS) - -**Characteristics:** -- Premium, high-resolution service -- Hourly data with 999-hour request window per call -- Requires JWT authentication -- Covers entire globe -- 15-minute resolution available - -**Data Acquisition:** -1. Obtain JWT token via SaaSCore authentication -2. Request hourly-on-demand (HOD) data in time chunks -3. Convert to timezone-aware UTC timestamps -4. Interpolate/aggregate to daily summaries - -**Cached Columns:** -- `temperature_min`, `temperature_max`, `temperature_mean` -- `temperatureDewPoint_min`, `temperatureDewPoint_max` -- `relativeHumidity_min`, `relativeHumidity_max` -- `windSpeed_max`, `windSpeed_mean` -- `precip1Hour_sum` (hourly accumulated) -- Rolling averages: 7, 14, 21, 30-day - ---- - -### WiscoNet (Public Mesonet) - -**Characteristics:** -- Wisconsin statewide mesonet network -- ~100 active weather stations -- Public API, no authentication required -- Daily data update cycle -- Station metadata includes: ID, name, lat/lng, county, region - -**Measurement Types:** -- Air temperature (min, avg, max) -- Dew point (min, avg, max) -- Relative humidity (%) -- Wind speed (max, direction) -- Precipitation (daily total) -- Soil temperature (select stations) -- Leaf wetness (select stations) - -**Data Retrieval:** -- Station list via `/wisconet_active_stations` -- Daily measurements via bulk query endpoint -- Cached in `station_measurements_cache/` (6-hour TTL) - ---- - -## Response Format - -### GeoJSON Feature Collection - -All responses follow the GeoJSON standard with extended properties: - -```json -{ - "type": "FeatureCollection", - "features": [ - { - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [-89.4, 43.1] - }, - "properties": { - "date": "2025-05-15", - "forecasting_date": "2025-05-15", - "station_id": "ALTN", - "station_name": "Alten", - "city": "Alten", - "county": "Columbia", - "region": "South Central", - "state": "WI", - "latitude": 43.1234, - "longitude": -89.4567, - "station_timezone": "America/Chicago", - - "temperature_min_c": 8.3, - "temperature_max_c": 21.5, - "temperature_mean_c": 15.2, - "relativeHumidity_max": 92, - "precip1Hour_sum": 2.5, - - "tarspot_risk": 0.45, - "tarspot_risk_class": "2.Moderate", - "gls_risk": 0.28, - "gls_risk_class": "1.Low", - "frogeye_risk": 0.12, - "frogeye_risk_class": "1.Low", - "whitemold_dry_risk": 0.35, - "whitemold_dry_risk_class": "2.Moderate", - - "biomass_lb_acre": 3250, - "biomass_color": "Yellow", - "biomass_message": "Moderate coverage" - } - } - ] -} -``` - -### Time-Series Grouping (WiscoNet) - -For multi-day forecasts, results are grouped by station with embedded time-series: - -```json -{ - "type": "FeatureCollection", - "features": [ - { - "type": "Feature", - "geometry": { "type": "Point", "coordinates": [-89.4, 43.1] }, - "properties": { - "station_id": "ALTN", - "station_name": "Alten", - "city": "Alten", - "county": "Columbia", - "region": "South Central", - "state": "WI", - "time_series": [ - { - "date": "2025-05-15", - "temperature_min_c": 8.3, - "tarspot_risk": 0.45, - ... - }, - { - "date": "2025-05-16", - "temperature_min_c": 7.8, - "tarspot_risk": 0.38, - ... - } - ] - } - } - ] -} -``` - ---- - -## Data Flow - -### Complete Request → Response Journey - -``` -User Request - │ - ▼ -┌──────────────────────────────┐ -│ Route Handler (app.py) │ -│ Validate query parameters │ -│ Extract coordinates/dates │ -└────────────────┬─────────────┘ - │ - ┌────────────┴────────────┐ - │ │ - ▼ ▼ -┌─────────────────┐ ┌───────────────┐ -│ IBM Service │ │ WiscoNet │ -│ │ │ Pipeline │ -│ 1. Authenticate │ │ │ -│ with IBM │ │ 1. Load │ -│ 2. Chunk time │ │ stations │ -│ range │ │ 2. Fetch │ -│ 3. Fetch hourly │ │ measurements -│ data │ │ 3. Merge │ -│ 4. Build daily │ │ metadata │ -│ summaries │ │ 4. Cache │ -│ │ │ results │ -└────────┬────────┘ └────────┬──────┘ - │ │ - └──────────┬───────────┘ - │ - ▼ - ┌──────────────────────┐ - │ Math Helpers │ - │ • Convert units │ - │ • GDD calculation │ - │ • Rolling averages │ - │ • Biomass prep │ - └──────────┬───────────┘ - │ - ▼ - ┌──────────────────────┐ - │ Risk Model Scoring │ - │ Apply all diseases │ - │ Compute probabilities│ - │ Classify risk class │ - └──────────┬───────────┘ - │ - ▼ - ┌──────────────────────┐ - │ Biomass Calculation │ - │ (if planting_date) │ - │ Logistic model │ - │ Color classification │ - └──────────┬───────────┘ - │ - ▼ - ┌──────────────────────┐ - │ GeoJSON Adapter │ - │ • Format geometry │ - │ • Embed properties │ - │ • Group by station │ - └──────────┬───────────┘ - │ - ▼ - Response (GeoJSON) -``` - ---- - -## Development Setup - -### Prerequisites - -- Python 3.10+ -- pip or conda -- Git -- Docker (optional, for containerized development) - -### Local Installation - -```bash -# Clone repository -git clone https://github.com/UW-Madison-DSI/ag_forecasting_api.git -cd ag_forecasting_api - -# Create virtual environment -python -m venv .venv -source .venv/bin/activate # On Windows: .venv\Scripts\activate - -# Install dependencies -pip install -r requirements.txt - -# Set environment variables -export IBM_API_KEY="your_key" -export TENANT_ID="your_tenant" -export ORG_ID="your_org" - -# Run development server -uvicorn app:app --reload --host 0.0.0.0 --port 8000 -``` - -### API Documentation - -Once running, view interactive documentation: -- **Swagger UI:** http://localhost:8000/docs -- **ReDoc:** http://localhost:8000/redoc -- **OpenAPI JSON:** http://localhost:8000/openapi.json - -### Testing - -```bash -# Syntax validation -python -m py_compile api/models/risk_models.py api/services/*.py - -# Run linting (if installed) -pylint api/ - -# Test endpoint with curl -curl "http://localhost:8000/v2/test/rye_biomass" -``` - ---- - -## Deployment - -### Docker Deployment - -```dockerfile -FROM python:3.11-slim -WORKDIR /app -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt -COPY . . -EXPOSE 8000 -CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"] -``` - -### Docker Compose (Production) - -```yaml -version: "3.9" -services: - ag_forecasting_api: - build: . - expose: - - 8000 - environment: - - PYTHONPATH=/app - - ENVIRONMENT=production - - IBM_API_KEY=${IBM_API_KEY} - - TENANT_ID=${TENANT_ID} - - ORG_ID=${ORG_ID} - networks: - - traefik - labels: - - "traefik.enable=true" - - "traefik.http.routers.ag_api.rule=Host(`your-domain.com`)" - - "traefik.http.services.ag_api.loadbalancer.server.port=8000" - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8000/"] - interval: 30s - timeout: 10s - retries: 3 -``` - -### Environment Variables - -Required for production: -- `IBM_API_KEY`: IBM EIS authentication credential -- `TENANT_ID`: IBM EIS tenant identifier -- `ORG_ID`: IBM EIS organization identifier -- `ENVIRONMENT`: Set to "production" for optimized logging - -Optional: -- `PYTHONPATH`: Should include `/app` for imports -- `LOG_LEVEL`: Logging verbosity (default: INFO) - ---- - -## Performance Considerations - -### Caching Strategy - -- **Station Metadata**: 7-day TTL (rarely changes) -- **Measurements**: 6-hour TTL (updated daily by WiscoNet) -- **IBM Responses**: Variable (tied to forecast date relevance) -- **File System**: JSON cache in `api_cache/` for persistence - -### Concurrency - -- **Async Pipeline**: Multi-station queries use async/await for I/O efficiency -- **Parallel Risk Scoring**: Disease calculations use multiprocessing (4 workers default) -- **Connection Pooling**: HTTP clients maintain connection pools (max 50 concurrent) - -### Optimization Tips - -1. **Batch Requests**: Query multiple stations in one call rather than sequential individual queries -2. **Use WiscoNet**: Public API has lower latency than premium IBM service -3. **Limit Date Range**: Smaller date ranges reduce computation time -4. **Filter Diseases**: Specify disease models needed to reduce calculation overhead -5. **Cache Warming**: Pre-fetch station lists during off-peak hours - ---- - -## Additional Resources - -- **GitHub Repository**: https://github.com/UW-Madison-DSI/ag_forecasting_api -- **Interactive Dashboard**: https://connect.doit.wisc.edu/ag_forecasting/ -- **Example Notebook**: `/materials/example_callapi.ipynb` -- **University Contact**: UW-Madison Data Science Institute - ---- - -## License - -See LICENSE file for details. - ---- - -## Changelog - -### Version 2.0 (Current) - -- ✅ Winter rye biomass model integration -- ✅ Unified API versioning (v1 legacy, v2 current) -- ✅ Enhanced GeoJSON responses -- ✅ Improved async pipeline performance -- ✅ Extended documentation - -### Version 1.0 (Legacy) - -- Disease risk models for corn and soybean -- WiscoNet and IBM EIS integration -- Basic GeoJSON support - ---- - -**Last Updated:** May 2025 -**Maintained By:** UW-Madison Data Science Institute diff --git a/_build/html/_sources/blog/index.md b/_build/html/_sources/blog/index.md index ebc9baf..d30a5f0 100644 --- a/_build/html/_sources/blog/index.md +++ b/_build/html/_sources/blog/index.md @@ -1,11 +1,10 @@ # OpenLambda Blog - News, releases, and technical deep-dives from the OpenLambda project. +News, releases, and technical deep-dives from the OpenLambda project. ```{postlist} 10 - :format: "{title} — {author}, {date}" - :date: "%B %d, %Y" - :list-style: circle - :excerpts: - :sort: -``` \ No newline at end of file +:format: "{title} — {author}, {date}" +:date: "%B %d, %Y" +:list-style: circle +:excerpts: +``` diff --git a/_build/html/_sources/blog/post/2026-05-18-ag-forecasting-case-study.md b/_build/html/_sources/blog/post/2026-05-18-ag-forecasting-case-study.md index 3bdc1fd..fb36833 100644 --- a/_build/html/_sources/blog/post/2026-05-18-ag-forecasting-case-study.md +++ b/_build/html/_sources/blog/post/2026-05-18-ag-forecasting-case-study.md @@ -1,7 +1,7 @@ --- blogpost: true date: 2026-05-18 -author: Maria Oros, Tyler Caraza-Harter +author: maria-oros, tyler-caraza-harter category: Case Studies tags: case-study, agforecast, asgi, fastapi, openlambda language: en @@ -16,4 +16,126 @@ Our goal is to make an ever-growing set of applications deployable on OpenLambda Recently, we selected an agricultural forecasting application (AgForecast), developed by the [Data Science Institute at UW–Madison](https://dsi.wisc.edu/), to port to OpenLambda: . AgForecast is an interesting case study, because it implements its REST API using FastAPI, which in turn uses [ASGI](https://asgi.readthedocs.io/en/latest/), the so-called "spiritual successor" to WSGI, which we recently started supporting in OpenLambda. WSGI is the basis for popular Python web-programming packages such as Django and Flask; new ASGI support opens the door to an even broader range of applications. -In this post, we describe the challenges of porting AgForecast to OL, and four new features we added to OL to make deployment of similar applications in the future simpler. The features are built-in ASGI support, direct GitHub-to-OL deployments, OL function environment variables, and OL-based pip compilation. \ No newline at end of file +In this post, we describe the challenges of porting AgForecast to OL, and four new features we added to OL to make deployment of similar applications in the future simpler. The features are built-in ASGI support, direct GitHub-to-OL deployments, OL function environment variables, and OL-based pip compilation. + +## Background: Agricultural Forecasting API + +**Motivation.** The agricultural forecasting app is an open-source tool designed to address farmers' needs in a customizable way. We first built a backend infrastructure using FastAPI to serve crop disease forecasting models for corn and soybean, focusing on integration with Wisconet weather stations and on-demand model serving. We then developed a custom R Shiny front-end interface for farmer use. + +**Fit with OpenLambda.** We began exploring OpenLambda to leverage the benefits of serverless technology. Hosting our tool on this platform offers significant value not only to the developer community, but also to plant pathology practitioners and scientists who want to build on robust, nationwide infrastructure. + +**Technical overview.** The Ag Forecasting API is a FastAPI-based backend that serves crop disease forecasting models for corn and soybean across Wisconsin. It exposes two parallel data pipelines through a unified interface: one sourced from Wisconet, the public mesonet of weather stations across the state, and another from IBM Environmental Intelligence for point-location queries by latitude and longitude. The core logic lives in the `ag_models_wrappers` module, which dynamically pulls the daily and hourly weather variables each disease model requires for a given forecasting date, runs the risk calculations, and returns localized predictions. Currently supported models include Sporecaster (white mold in soybean, with dry and irrigated row-spacing variants), Tarspotter (tar spot of corn), gray leaf spot, and frogeye leaf spot — all based on peer-reviewed plant pathology research from UW–Madison. + +**Infrastructure and integration.** The project ships with a `Dockerfile` and `docker-compose.yml` for containerized deployment, and includes a Starlette `WSGIMiddleware` wrapper so the FastAPI app can also be served behind WSGI servers for legacy or mixed environments. A companion sub-package, `pywisconet`, provides a thin REST wrapper over the Wisconet v1 API with endpoints for active station discovery (`/wisconet/active_stations/`), station field metadata (`/station_fields/{station_id}`), and bulk measurement retrieval (`/bulk_measures/{station_id}`) at configurable frequencies (5-minute, hourly, or daily) for variables like air temperature, dew point, wind speed, and relative humidity. IBM credentials are handled via environment variables (`IBM_API_KEY`, `TENANT_ID`, `ORG_ID`) and validated against query parameters at request time. The API is MIT-licensed, fully open source, and currently deployed at [connect.doit.wisc.edu/ag_forecasting_api](https://connect.doit.wisc.edu/ag_forecasting_api). + +## Porting to OpenLambda + +When porting AgForecast to OL, we encountered 5 challenges related to: expectations about writable directories, package version selection, deployment from GitHub, asynchronous execution, and parallel pool execution. To overcome these challenges, we introduced four new features to OL and made minor changes to AgForecast itself. + +### Challenge 1: File Management + +AgForecast is semi-stateless: data files describing stations and measurements are used across requests, but if these files are deleted, it can generate them on-the-fly from upstream data sources, such as the IBM Weather API or Wisconet. This is a good match for FaaS platforms such as OL, where lambda instances frequently persist (along with their state) across multiple invocations, even if an instance can silently be terminated at any time to reclaim memory. + +However, most directory locations are read-only for an OL function; the one exception is a single "scratch directory" and various other locations that are symbolic links to the scratch directory (such as `/tmp`). Full-featured sandboxes such as Docker containers use union file systems to make many directories editable, on a copy-on-write basis. OL's limitation is due to its use of bind mounts, a leaner, but less flexible mechanism. AgForecast wasn't originally built for OL, so we modified the code to make the directory location for station and measurement data configurable via environment variables (otherwise AgForecast attempted to write to read-only locations). We added support to OL function configuration files to support the specification of environment variables, like this: + +```yaml +triggers: + http: + - method: "*" +environment: + MEASUREMENTS_CACHE_DIR: /host/tmp/cache + STATIONS_CACHE_FILE: /host/tmp/cache/wisconsin_stations_cache.csv + ... +``` + +### Challenge 2: Package Management + +Like many Python projects, AgForecast specifies PyPI package requirements in a `requirements.txt` file. Also like most projects, not all version requirements are exact. Here are 3 of the 21 lines in AgForecast's `requirements.txt`: + +``` +... +matplotlib==3.9.1 +fastapi>=0.95.0 +pydantic +... +``` + +Note the different levels of specificity: `matplotlib` must be a specific version, whereas pip can select any version for `pydantic` (probably the latest, barring version conflicts based on other dependencies); `fastapi` specifies a range. Note that some of these might also have indirect dependencies on other packages not explicitly listed. + +OL requires exact versions for all packages, direct or indirect. For this purpose, we recommend the use of [`pip-compile`](https://pypi.org/project/pip-tools/) to translate a partially specified `requirements.txt` file to a fully specified one, based on the latest packages at the time of compilation. + +One challenge is that pip and pip-compile sometimes select packages based on the host environment. For example, in certain cases, the suitable Python package version for Ubuntu 24.04 might be different than for Ubuntu 26.04. Initially, we encountered this when deploying AgForecast on OL. The `requirements.txt` created by doing pip-compile on the host machine was not quite compatible with the environment inside the lambda function. To address this, we created a new OL function that does pip-compile inside the OL environment. It works like this: + +```bash +curl -X POST -d '' http://localhost:5000/run/pip-compile/url > requirements.txt +``` + +### Challenge 3: Deployment + +AgForecast lives on a public GitHub repo: . We wanted to make it as easy as possible to deploy directly from GitHub to an OL function. Thus, we added a new `ol admin install` option to point directly to a repo: + +```bash +./ol admin install -c ol.yaml -r requirements.txt https://github.com//.git +``` + +### Challenge 4: Asynchronous Execution + +AgForecast is built on FastAPI, which in turn is based on ASGI. ASGI is an asynchronous alternative to WSGI (Web Server Gateway Interface). The idea of WSGI is to let you mix and match servers (for example, Gunicorn, uWSGI) with application frameworks (for example, Flask, Django). The server/framework interface is minimalist, a single function signature that the server calls for each incoming HTTP request (GET/POST/etc). The framework implements the function; a common framework pattern is to route the call to a user-written handler function. For example, consider these two functions: + +```python +app = Flask(__name__) + +@app.route("/") +def home(): + return "Home page" + +@app.route("/about") +def about(): + return "About page" +``` + +The `app` object is a Python callable (meaning it is an object that acts like a function); `app` implements the WSGI interface. So when a server sends a request to `app`, `app` in turn calls the correct user function (`home` or `about`) to execute and obtain a result. + +ASGI was introduced as an alternative to WSGI to provide more options for handling concurrent calls. Consider how (in the above Flask/WSGI example) two different users may want to visit the home and about pages at the same time. Can we handle the requests concurrently? + +There are a few ways to do this: multiple processes, multiple threads in a process, or [Python's async functionality introduced in Python 3.5 (2015)](https://peps.python.org/pep-0492/). If we want all execution in a single process (useful when there is shared state), we can either use threading (with WSGI) or async (with ASGI). + +Threads are a non-cooperative form of scheduling, meaning that a scheduler can switch from running one thread on a CPU to another thread at any time (perhaps a very inconvenient time!). Writing multi-threaded programs is notoriously difficult, as one must identify shared state, introduce locks to protect that state, and acquire/release locks at the right points. In contrast, async offers a form of cooperative scheduling, where switches can only occur at well-defined points (e.g., an `await`). Thus, programming is simpler (no need for locks). + +Normally, concurrent programs allow (a) parallel execution on multiple CPU cores at the same time and (b) execution of code at the same time that input/output occurs, say to the disk or network. In many languages, threads offer both benefits and cooperative scheduling only provides the I/O benefit. However, Python threads only offer the I/O benefit due to the GIL (Global Interpreter Lock), [though this may be changing](https://docs.python.org/3/howto/free-threading-python.html). Thus, async is especially appealing in Python since it (in theory) matches threading in terms of performance benefits, and surpasses threads in terms of ease-of-programming. + +AgForecast implements REST calls in FastAPI, which is based on ASGI, which is the async-based alternative to WSGI. To support AgForecast, we implemented ASGI server functionality in OpenLambda. This makes OpenLambda an ASGI server implementation (in the same role as Uvicorn, Gunicorn, Daphne, etc). When a request arrives, OpenLambda uses `asyncio.run(...)` to invoke the user-provided entry point, with async send/receive callbacks. The user-provided entry-point can then be a full application, written in any ASGI-compatible framework (FastAPI, Starlette, Django/Channels, etc). + +A user can indicate their lambda function is an ASGI application entry point by configuring their `ol.yaml` as follows: + +```yaml +environment: + OL_ENTRY_FILE: app.py + OL_ASGI_ENTRY: app +``` + +### Challenge 5: Worker Pools + +AgForecast indirectly uses `/dev/shm`, an in-memory file system called tmpfs. Docker containers have a `/dev/shm` mount by default; as it is frequently used for inter-process communication, its behavior is configurable via the `--ipc` flag. In contrast, OL functions do not have any `/dev/shm` mount. + +Why does AgForecast need `/dev/shm`? AgForecast uses a `concurrent.futures.ProcessPoolExecutor` to run a Python compute-heavy function called `compute_risks` in parallel over different chunks of data. `ProcessPoolExecutor` creates different Python processes for different chunks of work, which is desirable as a way to get around Python's per-process GIL (Global Interpreter Lock), which is held whenever regular Python code is being executed. With multiple processes (created by the pool), each process will have its own GIL that it holds during execution; thus, multiple processes can hold their own locks at the same time and execute on multiple CPU cores in parallel. + +As a fix to get AgForecast working as an OL function, we replaced `ProcessPoolExecutor` with a `ThreadPoolExecutor`; this avoids inter-process communication via `/dev/shm` because coordination occurs within a process, between threads. Unfortunately, all the threads share a GIL, so the performance benefits of using a pool for parallelism is lost in this case (the only value would be if `compute_risks` were I/O heavy, which it is not). + +Using a `ThreadPoolExecutor` works as a short term fix, but this experience suggests that eventually adding `/dev/shm` availability (and thus `ProcessPoolExecutor` functionality) would be a useful future feature for OL. + +## Recap of New Features + +Porting real, complex applications to serverless platforms highlights the most important features to develop. In this post, we described 5 challenges we encountered when porting AgForecast to OL, and the following features we added to better support similar applications: + +- environment variable configuration for lambda functions (challenge 1) +- pip-compile as a lambda function (challenge 2) +- direct GitHub deployment (challenge 3) +- ASGI support (challenge 4) + +We also made some minor changes to AgForecast: + +- customizable directory use for stations/measurements (challenge 1) +- use of a thread pool instead of a process pool (challenge 5) + +The second change suggests a future possible OL feature: adding `/dev/shm` availability to support process pools. \ No newline at end of file diff --git a/_build/html/_sources/worker.md b/_build/html/_sources/worker.md index ff4f29e..ff2d221 100644 --- a/_build/html/_sources/worker.md +++ b/_build/html/_sources/worker.md @@ -58,5 +58,5 @@ multi-node setups. ## Further Reading -- [Quickstart guide](doc.htm) — get a single worker running locally in minutes +- [Quickstart guide](https://github.com/open-lambda/open-lambda/blob/main/docs/quickstart.md) — get a single worker running locally in minutes - [SOCK: Rapid Task Provisioning with Serverless-Optimized Containers](https://www.usenix.org/conference/atc18/presentation/oakes) — the research paper describing the container backend. \ No newline at end of file diff --git a/_build/html/ag-forecasting-api.html b/_build/html/ag-forecasting-api.html deleted file mode 100644 index 1f0b844..0000000 --- a/_build/html/ag-forecasting-api.html +++ /dev/null @@ -1,1627 +0,0 @@ - - - - - - - - - - - Agricultural Forecasting API - Technical Documentation — OpenLambda - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - -
-
-
-
-
- -
- -
- - - - - -
-
- - - -
- - - - - - - - - - - - - -
- -
- - - -
- -
-
- -
-
- -
- -
- -
- - -
- -
- -
- - - - - - - - - - - - - - - - - - - -
- -
- -
-
- - - - - - - - -
- -
-

Agricultural Forecasting API - Technical Documentation#

-

A FastAPI-based service for crop disease forecasting and biomass prediction using weather data integration from multiple sources. Developed by the University of Wisconsin-Madison Data Science Institute.

-
-
-

Table of Contents#

-
    -
  1. Overview

  2. -
  3. Architecture

  4. -
  5. Technology Stack

  6. -
  7. API Endpoints

  8. -
  9. Core Features

  10. -
  11. Data Flow

  12. -
  13. Disease Models

  14. -
  15. Biomass Models

  16. -
  17. Weather Data Sources

  18. -
  19. Response Format

  20. -
  21. Development Setup

  22. -
  23. Deployment

  24. -
-
-
-
-

Overview#

-

The Agricultural Forecasting API provides real-time crop disease risk predictions and winter rye biomass estimates for Wisconsin agricultural locations. The system integrates historical weather data with scientifically-validated disease forecasting models to help farmers and agricultural professionals make informed management decisions.

-

Key Capabilities:

-
    -
  • Real-time disease risk predictions for corn and soybean

  • -
  • Winter rye biomass accumulation forecasts

  • -
  • Multi-source weather data integration (IBM EIS, WiscoNet, NOAA)

  • -
  • Geospatial queries by coordinates or weather station ID

  • -
  • Batch processing for multi-station analysis

  • -
  • GeoJSON-formatted responses for mapping integration

  • -
-
-
-
-

Architecture#

-
-

High-Level Design#

-
┌─────────────────────────────────────────────────────────────┐
-│                     FastAPI Application                      │
-├─────────────────────────────────────────────────────────────┤
-│                                                               │
-│  ┌─────────────────┐  ┌──────────────────┐  ┌─────────────┐ │
-│  │   /v2/ibm       │  │  /v2/wisconet_g  │  │  /v2/models │ │
-│  │  (Coordinates)  │  │  (Station-based) │  │  (Metadata) │ │
-│  └────────┬────────┘  └────────┬─────────┘  └─────────────┘ │
-│           │                    │                             │
-├───────────┼────────────────────┼─────────────────────────────┤
-│           │                    │                             │
-│  ┌────────▼────────────┐  ┌───▼──────────────────┐           │
-│  │  IBM Service        │  │  Pipeline / WiscoNet │           │
-│  │  (ibm_service.py)   │  │  (pipeline.py)       │           │
-│  │                     │  │                      │           │
-│  │ • Authenticate      │  │ • Load stations      │           │
-│  │ • Fetch raw data    │  │ • Fetch measurements │           │
-│  │ • Build hourly      │  │ • Compute biomass    │           │
-│  │ • Build daily       │  │ • Annotate risks     │           │
-│  │ • Compute rolling   │  │                      │           │
-│  │   averages          │  │                      │           │
-│  └────────┬────────────┘  └───┬──────────────────┘           │
-│           │                   │                              │
-├───────────┼───────────────────┼──────────────────────────────┤
-│           │                   │                              │
-│  ┌────────▼─────────────────────┴──────┐                     │
-│  │     Risk Models & Biomass Calc       │                    │
-│  │     (api/models/risk_models.py)      │                    │
-│  │                                      │                    │
-│  │ • Tarspot risk (corn)                │                    │
-│  │ • Gray Leaf Spot (corn)              │                    │
-│  │ • Frogeye Leaf Spot (soybean)        │                    │
-│  │ • White Mold (soybean)               │                    │
-│  │ • Winter Rye Biomass (logistic)      │                    │
-│  └────────┬─────────────────────────────┘                    │
-│           │                                                  │
-├───────────┼──────────────────────────────────────────────────┤
-│           │                                                  │
-│  ┌────────▼────────────────────────────┐                     │
-│  │  Output Formatting & Response        │                    │
-│  │  (adapters/geojson_adapter.py)       │                    │
-│  │                                      │                    │
-│  │ • GeoJSON Feature Collections        │                    │
-│  │ • Time-series grouping               │                    │
-│  │ • Station metadata embedding         │                    │
-│  └──────────────────────────────────────┘                    │
-│                                                               │
-└─────────────────────────────────────────────────────────────┘
-
-
-
-
-

Directory Structure#

-
ag_forecasting_api/
-├── app.py                          # Main FastAPI entry point (v2)
-├── app_v1.py                       # Legacy API (deprecated, v1)
-├── requirements.txt                # Python dependencies
-├── Dockerfile                      # Container image definition
-├── docker-compose.yml              # Multi-container orchestration
-│
-├── api/                            # Core business logic
-│   ├── __init__.py
-│   ├── config/
-│   │   └── constants.py            # Configuration, URLs, thresholds
-│   │
-│   ├── models/
-│   │   ├── risk_models.py          # Disease & biomass calculations
-│   │   └── disease_metadata.py     # Model metadata registry
-│   │
-│   ├── services/
-│   │   ├── ibm_service.py          # IBM EIS weather integration
-│   │   ├── wisconet_service.py     # WiscoNet station pipeline
-│   │   └── risk_processor.py       # Risk annotation orchestration
-│   │
-│   ├── routes/
-│   │   ├── wisconet.py             # /v2/ag_models_wrappers/wisconet_g
-│   │   └── models.py               # /v2/ag_models_wrappers/models
-│   │
-│   ├── adapters/
-│   │   └── geojson_adapter.py      # DataFrame → GeoJSON converter
-│   │
-│   ├── schemas/
-│   │   ├── geojson_schema.py       # Response schema definitions
-│   │   └── model_schema.py         # Model metadata schemas
-│   │
-│   ├── utils/
-│   │   ├── math_helpers.py         # GDD, logistic, rolling averages
-│   │   ├── conversions.py          # Unit conversions (F↔C, mph↔m/s)
-│   │   └── caching.py              # Station & measurement caching
-│   │
-│   └── pipeline.py                 # Orchestrates WiscoNet workflow
-│
-├── api_cache/                      # Cached API responses (JSON)
-│   ├── metadata_stations_*.json
-│   └── station_data_*.json
-│
-└── materials/
-    └── example_callapi.ipynb       # Usage examples
-
-
-
-
-

Module Responsibilities#

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Module

Purpose

Key Functions

ibm_service.py

IBM weather API integration

get_weather_with_risk(), _fetch_raw_hourly(), _build_daily(), _annotate_winter_rye_biomass()

wisconet_service.py

WiscoNet station data pipeline

_compute_winter_rye_biomass(), station loading & filtering

risk_models.py

Disease & biomass calculations

calculate_tarspot_risk(), calculate_winter_rye_biomass(), etc.

pipeline.py

Async WiscoNet orchestration

retrieve(), multi-station batch processing

math_helpers.py

Shared mathematical utilities

gdd_sine(), logistic(), rolling_mean()

geojson_adapter.py

Response formatting

dataframe_to_featurecollection()

-
-
-
-
-
-

Technology Stack#

-
-

Core Dependencies#

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Component

Library

Version

Purpose

Web Framework

FastAPI

^0.100

REST API framework

ASGI Server

Uvicorn

^0.24

ASGI server

Data Processing

Pandas

^2.0

Tabular data manipulation

Numeric Computing

NumPy

^1.24

Numerical calculations

Data Validation

Pydantic

^2.0

Request/response schema validation

HTTP Client

requests

^2.31

Synchronous HTTP calls

Async HTTP

aiohttp

^3.9

Async HTTP requests

Logging

Python logging

stdlib

Structured logging

-
-
-
-

External Services#

-
    -
  • IBM Environmental Intelligence Suite (EIS): Premium weather API with historical-on-demand (HOD) data

  • -
  • WiscoNet: Public Wisconsin mesonet weather station network

  • -
  • NOAA: National weather service data for historical context

  • -
-
-
-

Deployment#

-
    -
  • Container: Docker with Alpine Linux base

  • -
  • Orchestration: Docker Compose for multi-service deployments

  • -
  • Reverse Proxy: Traefik for URL routing and TLS termination

  • -
  • Health Monitoring: Periodic curl-based health checks

  • -
-
-
-
-
-

API Endpoints#

-
-

V2 Endpoints (Current)#

-
-

Disease Risk & Biomass by Coordinates (IBM)#

-
GET /v2/ag_models_wrappers/ibm
-
-
-

Query parameters:

-
    -
  • forecasting_date (required): Reference date (YYYY-MM-DD)

  • -
  • latitude (required): Decimal degrees

  • -
  • longitude (required): Decimal degrees

  • -
  • planting_date (optional): For biomass calculation (YYYY-MM-DD)

  • -
  • termination_date (optional): For WiscoNet biomass integration (YYYY-MM-DD)

  • -
  • API_KEY (required): IBM EIS credential

  • -
  • TENANT_ID (required): IBM EIS credential

  • -
  • ORG_ID (required): IBM EIS credential

  • -
-

Example:

-
curl "http://localhost:8000/v2/ag_models_wrappers/ibm?forecasting_date=2025-05-15&latitude=43.0&longitude=-89.0&planting_date=2024-09-15&API_KEY=xxx&TENANT_ID=yyy&ORG_ID=zzz"
-
-
-

Response: GeoJSON FeatureCollection with hourly and daily weather data plus risk scores.

-
-
-
-

Disease Risk & Biomass by WiscoNet Stations#

-
GET /v2/ag_models_wrappers/wisconet_g
-
-
-

Query parameters:

-
    -
  • forecasting_date (required): Reference date (YYYY-MM-DD)

  • -
  • risk_days (optional): Days to compute rolling risk (default: 1)

  • -
  • station_id (optional): Filter to single station (e.g., “ALTN”)

  • -
  • planting_date (optional): For winter rye biomass (YYYY-MM-DD)

  • -
  • termination_date (optional): For crop termination biomass (YYYY-MM-DD)

  • -
  • disease (optional): Filter to specific disease model

  • -
-

Example:

-
curl "http://localhost:8000/v2/ag_models_wrappers/wisconet_g?forecasting_date=2025-05-15&station_id=ALTN&planting_date=2024-09-15"
-
-
-

Response: GeoJSON FeatureCollection grouped by station with time-series risk data.

-
-
-
-

Model Metadata#

-
GET /v2/ag_models_wrappers/models
-
-
-

Returns list of all available disease models with descriptions, crops, risk categories, and input requirements.

-
-
-
-
-

Legacy V1 Endpoints#

-

The API maintains backward compatibility with v1 endpoints (mounted at /v1), including:

-
    -
  • /v1/sites - Station listing

  • -
  • /v1/predictions - Disease predictions

  • -
  • /v1/models - Model metadata

  • -
-
-
-
-
-

Core Features#

-
-

1. Multi-Source Weather Integration#

-

The API combines weather data from multiple sources with intelligent fallback:

-
┌──────────────────────────────────────────────────┐
-│ Weather Data Request (lat/lng, date range)       │
-└────────────────┬─────────────────────────────────┘
-                 │
-     ┌───────────┴──────────────┐
-     │                          │
-     ▼                          ▼
-┌─────────────────┐      ┌──────────────────┐
-│ IBM EIS API     │      │ WiscoNet Network │
-│ (Premium,       │      │ (Public,         │
-│ high res)       │      │ station-based)   │
-└────────┬────────┘      └────────┬─────────┘
-         │                        │
-         └────────────┬───────────┘
-                      │
-                      ▼
-            ┌──────────────────────┐
-            │ Data Harmonization   │
-            │ • Timezone aware     │
-            │ • Unit conversion    │
-            │ • Interpolation      │
-            └──────────┬───────────┘
-                       │
-                       ▼
-            ┌──────────────────────┐
-            │ Growing Degree Days  │
-            │ • Sine method        │
-            │ • Base: 0°C, Cap: 35°C│
-            └──────────┬───────────┘
-                       │
-                       ▼
-            ┌──────────────────────┐
-            │ Rolling Averages     │
-            │ • 7, 14, 21, 30-day  │
-            └──────────┬───────────┘
-                       │
-                       ▼
-            ┌──────────────────────┐
-            │ Risk Model Input     │
-            │ (Ready for scoring)  │
-            └──────────────────────┘
-
-
-
-
-

2. Efficient Data Caching#

-

Reduces redundant API calls through tiered caching:

-
# Cache layers (in api/utils/caching.py)
-- Station metadata: 7-day TTL
-- Measurements: 6-hour TTL
-- IBM responses: Variable TTL based on forecast date
-- File system: JSON cache in api_cache/ directory
-
-
-
-
-

3. Async Pipeline Processing#

-

Multi-station workflows use async processing for performance:

-
# pipeline.py
-async def _run_pipeline(...):
-    # 1. Load all active stations (filtered)
-    stations = load_stations_sync(...)
-    
-    # 2. Fetch all measurements concurrently
-    measurements = await fetch_all_measurements_async(stations)
-    
-    # 3. Merge metadata (station location, timezone, etc.)
-    merged = merge_station_metadata(measurements, stations)
-    
-    # 4. Compute winter rye biomass (if planting_date provided)
-    if planting_date:
-        biomass = _compute_winter_rye_biomass(merged, planting_date)
-        merged = merge_biomass(merged, biomass)
-    
-    # 5. Optionally fetch WiscoNet-specific biomass data
-    if both planting_date and termination_date:
-        wisconet_biomass = await fetch_wisconet_biomass(...)
-        merged = merge_wisconet_biomass(merged, wisconet_biomass)
-    
-    # 6. Compute disease risks in parallel
-    risk_df = compute_risks_in_parallel(
-        merged, num_workers=4
-    )
-    
-    return risk_df
-
-
-
-
-

4. Intelligent Date & Timezone Handling#

-
    -
  • Automatic timezone conversion to station local time

  • -
  • Date arithmetic aware of DST transitions

  • -
  • Support for both absolute dates and relative forecasts

  • -
-
-
-
-
-

Disease Models#

-
-

Tarspot Risk (Corn)#

-

Crop: Corn
-Disease: Phyllachora maydis (Tar Spot fungus)

-

Model Type: Ensemble logistic regression (two sub-models averaged)

-

Inputs:

-
    -
  • 30-day mean air temperature (°C)

  • -
  • 30-day max relative humidity (%)

  • -
  • 14-day mean nighttime hours with RH ≥ 90%

  • -
-

Output: Risk probability (0–1), classified as:

-
    -
  • Inactive (mean temp < 10°C)

  • -
  • 1.Low (prob < 0.20)

  • -
  • 2.Moderate (prob 0.20–0.35)

  • -
  • 3.High (prob > 0.35)

  • -
-

Reference: Damon et al.

-
-
-
-

Gray Leaf Spot Risk (Corn)#

-

Crop: Corn
-Disease: Cercospora zeae-maydis (Gray Leaf Spot)

-

Model Type: Single logistic regression

-

Inputs:

-
    -
  • 21-day min air temperature (°C)

  • -
  • 30-day min dew point (°C)

  • -
-

Assumptions: Growth stage V10–R3, no irrigation

-

Output: Risk probability (0–1), same classification as Tarspot

-
-
-
-

Frogeye Leaf Spot Risk (Soybean)#

-

Crop: Soybean
-Disease: Cercospora sojina (Frogeye Leaf Spot)

-

Model Type: Logistic regression

-

Inputs:

-
    -
  • Degree-days above 14°C (cumulative from emergence)

  • -
  • Rainfall in previous 14 days (mm)

  • -
  • Hours with RH ≥ 85% (14-day rolling)

  • -
-

Output: Risk probability (0–1)

-
-
-
-

White Mold Risk (Soybean)#

-

Crop: Soybean
-Disease: Sclerotinia sclerotiorum (White Mold / Sclerotinia)

-

Model Type: Dual variant (irrigated & non-irrigated)

-

Inputs:

-
    -
  • Days since planting

  • -
  • Cumulative precipitation (mm)

  • -
  • Soil moisture availability

  • -
-

Variants:

-
    -
  • Dry: Non-irrigated fields

  • -
  • Irrigated 15”: 15-inch row spacing

  • -
  • Irrigated 30”: 30-inch row spacing

  • -
-

Output: Apothecial presence probability (0–1)

-
-
-
-

Winter Rye Biomass (NEW)#

-

Crop: Winter Rye
-Metric: Aboveground dry biomass (lb/acre)

-

Model Type: Logistic growth model

-

Formula:

-
-\[\text{logit} = b_0 + b_{pd} \cdot \text{plant\_doy} + b_{pf} \cdot \text{precip\_fall}\]
-
-\[\text{pred} = \frac{\text{logit}}{1 + e^{-k(\text{gdd\_total}-x_0)}}\]
-
-\[\text{biomass} = \max(0, \text{pred}^2)\]
-

Coefficients:

-
    -
  • \(b_0 = 423.1\) (intercept)

  • -
  • \(b_{pd} = -1.031\) (day-of-year effect)

  • -
  • \(b_{pf} = -0.2878\) (fall precipitation effect)

  • -
  • \(k = 0.003663\) (logistic rate)

  • -
  • \(x_0 = 1049.0\) (inflection point GDD)

  • -
-

Inputs:

-
    -
  • Planting day-of-year (1–366)

  • -
  • Precipitation during fall establishment (mm)

  • -
  • Cumulative GDD from planting to current date (0°C base)

  • -
-

Output:

-
    -
  • Biomass (lb/acre): Numeric prediction

  • -
  • Biomass Color:

    -
      -
    • Gray: 0–2000 lb/acre (low coverage)

    • -
    • Yellow: 2000–4500 lb/acre (moderate)

    • -
    • Green: >4500 lb/acre (high/dense)

    • -
    -
  • -
  • Biomass Message: Human-readable interpretation

  • -
-

Use Cases:

-
    -
  • Cover crop management planning

  • -
  • Soil health assessment

  • -
  • Erosion mitigation evaluation

  • -
  • Residue management decisions

  • -
-
-
-
-
-

Biomass Models#

-
-

Winter Rye Biomass Calculation Pipeline#

-
def _annotate_winter_rye_biomass(
-    rye_daily: pd.DataFrame,
-    planting_date: str
-) -> pd.DataFrame:
-    """
-    Compute winter rye biomass from daily weather data.
-    
-    Steps:
-    1. Parse planting_date → day-of-year
-    2. Filter to dates >= planting_date
-    3. Compute daily GDD (sine method, 0°C base, 35°C cap)
-    4. Accumulate GDD and precipitation
-    5. Call calculate_winter_rye_biomass() for each row
-    6. Merge results back into DataFrame
-    """
-    
-    # Step 1: Parse planting date
-    plant_date_obj = datetime.fromisoformat(planting_date).date()
-    plant_doy = plant_date_obj.timetuple().tm_yday
-    
-    # Step 2: Filter to >= planting date
-    rye_daily = rye_daily[rye_daily['date'] >= planting_date].copy()
-    
-    # Step 3-4: GDD accumulation
-    rye_daily['plant_doy'] = plant_doy
-    rye_daily['precip_daily'] = rye_daily['precip1Hour_sum'].fillna(0)
-    rye_daily['gdd_0c'] = rye_daily.apply(
-        lambda row: gdd_sine(row['temperature_min'], row['temperature_max']),
-        axis=1
-    )
-    rye_daily['gdd_total'] = rye_daily['gdd_0c'].cumsum()
-    
-    # Compute fall precipitation (Sep 1 to planting)
-    fall_start = plant_date_obj.replace(month=9, day=1)
-    fall_precip = rye_daily[
-        (rye_daily['date'] >= str(fall_start)) & 
-        (rye_daily['date'] < planting_date)
-    ]['precip_daily'].sum()
-    
-    # Step 5: Model calculation
-    biomass_results = rye_daily.apply(
-        lambda row: calculate_winter_rye_biomass(
-            plant_doy=row['plant_doy'],
-            precip_fall=fall_precip,
-            gdd_total=row['gdd_total']
-        ),
-        axis=1,
-        result_type='expand'
-    )
-    
-    # Step 6: Merge results
-    for col in ['biomass_lb_acre', 'biomass_color', 'biomass_message']:
-        rye_daily[col] = biomass_results[col]
-    
-    return rye_daily
-
-
-
-
-
-
-

Weather Data Sources#

-
-

IBM Environmental Intelligence Suite (EIS)#

-

Characteristics:

-
    -
  • Premium, high-resolution service

  • -
  • Hourly data with 999-hour request window per call

  • -
  • Requires JWT authentication

  • -
  • Covers entire globe

  • -
  • 15-minute resolution available

  • -
-

Data Acquisition:

-
    -
  1. Obtain JWT token via SaaSCore authentication

  2. -
  3. Request hourly-on-demand (HOD) data in time chunks

  4. -
  5. Convert to timezone-aware UTC timestamps

  6. -
  7. Interpolate/aggregate to daily summaries

  8. -
-

Cached Columns:

-
    -
  • temperature_min, temperature_max, temperature_mean

  • -
  • temperatureDewPoint_min, temperatureDewPoint_max

  • -
  • relativeHumidity_min, relativeHumidity_max

  • -
  • windSpeed_max, windSpeed_mean

  • -
  • precip1Hour_sum (hourly accumulated)

  • -
  • Rolling averages: 7, 14, 21, 30-day

  • -
-
-
-
-

WiscoNet (Public Mesonet)#

-

Characteristics:

-
    -
  • Wisconsin statewide mesonet network

  • -
  • ~100 active weather stations

  • -
  • Public API, no authentication required

  • -
  • Daily data update cycle

  • -
  • Station metadata includes: ID, name, lat/lng, county, region

  • -
-

Measurement Types:

-
    -
  • Air temperature (min, avg, max)

  • -
  • Dew point (min, avg, max)

  • -
  • Relative humidity (%)

  • -
  • Wind speed (max, direction)

  • -
  • Precipitation (daily total)

  • -
  • Soil temperature (select stations)

  • -
  • Leaf wetness (select stations)

  • -
-

Data Retrieval:

-
    -
  • Station list via /wisconet_active_stations

  • -
  • Daily measurements via bulk query endpoint

  • -
  • Cached in station_measurements_cache/ (6-hour TTL)

  • -
-
-
-
-
-

Response Format#

-
-

GeoJSON Feature Collection#

-

All responses follow the GeoJSON standard with extended properties:

-
{
-  "type": "FeatureCollection",
-  "features": [
-    {
-      "type": "Feature",
-      "geometry": {
-        "type": "Point",
-        "coordinates": [-89.4, 43.1]
-      },
-      "properties": {
-        "date": "2025-05-15",
-        "forecasting_date": "2025-05-15",
-        "station_id": "ALTN",
-        "station_name": "Alten",
-        "city": "Alten",
-        "county": "Columbia",
-        "region": "South Central",
-        "state": "WI",
-        "latitude": 43.1234,
-        "longitude": -89.4567,
-        "station_timezone": "America/Chicago",
-        
-        "temperature_min_c": 8.3,
-        "temperature_max_c": 21.5,
-        "temperature_mean_c": 15.2,
-        "relativeHumidity_max": 92,
-        "precip1Hour_sum": 2.5,
-        
-        "tarspot_risk": 0.45,
-        "tarspot_risk_class": "2.Moderate",
-        "gls_risk": 0.28,
-        "gls_risk_class": "1.Low",
-        "frogeye_risk": 0.12,
-        "frogeye_risk_class": "1.Low",
-        "whitemold_dry_risk": 0.35,
-        "whitemold_dry_risk_class": "2.Moderate",
-        
-        "biomass_lb_acre": 3250,
-        "biomass_color": "Yellow",
-        "biomass_message": "Moderate coverage"
-      }
-    }
-  ]
-}
-
-
-
-
-

Time-Series Grouping (WiscoNet)#

-

For multi-day forecasts, results are grouped by station with embedded time-series:

-
{
-  "type": "FeatureCollection",
-  "features": [
-    {
-      "type": "Feature",
-      "geometry": { "type": "Point", "coordinates": [-89.4, 43.1] },
-      "properties": {
-        "station_id": "ALTN",
-        "station_name": "Alten",
-        "city": "Alten",
-        "county": "Columbia",
-        "region": "South Central",
-        "state": "WI",
-        "time_series": [
-          {
-            "date": "2025-05-15",
-            "temperature_min_c": 8.3,
-            "tarspot_risk": 0.45,
-            ...
-          },
-          {
-            "date": "2025-05-16",
-            "temperature_min_c": 7.8,
-            "tarspot_risk": 0.38,
-            ...
-          }
-        ]
-      }
-    }
-  ]
-}
-
-
-
-
-
-
-

Data Flow#

-
-

Complete Request → Response Journey#

-
User Request
-    │
-    ▼
-┌──────────────────────────────┐
-│ Route Handler (app.py)       │
-│ Validate query parameters    │
-│ Extract coordinates/dates    │
-└────────────────┬─────────────┘
-                 │
-    ┌────────────┴────────────┐
-    │                         │
-    ▼                         ▼
-┌─────────────────┐    ┌───────────────┐
-│ IBM Service     │    │ WiscoNet      │
-│                 │    │ Pipeline      │
-│ 1. Authenticate │    │               │
-│    with IBM     │    │ 1. Load       │
-│ 2. Chunk time   │    │    stations   │
-│    range        │    │ 2. Fetch      │
-│ 3. Fetch hourly │    │    measurements
-│    data         │    │ 3. Merge      │
-│ 4. Build daily  │    │    metadata   │
-│    summaries    │    │ 4. Cache      │
-│                 │    │    results    │
-└────────┬────────┘    └────────┬──────┘
-         │                      │
-         └──────────┬───────────┘
-                    │
-                    ▼
-         ┌──────────────────────┐
-         │ Math Helpers         │
-         │ • Convert units      │
-         │ • GDD calculation    │
-         │ • Rolling averages   │
-         │ • Biomass prep       │
-         └──────────┬───────────┘
-                    │
-                    ▼
-         ┌──────────────────────┐
-         │ Risk Model Scoring   │
-         │ Apply all diseases   │
-         │ Compute probabilities│
-         │ Classify risk class  │
-         └──────────┬───────────┘
-                    │
-                    ▼
-         ┌──────────────────────┐
-         │ Biomass Calculation  │
-         │ (if planting_date)   │
-         │ Logistic model       │
-         │ Color classification │
-         └──────────┬───────────┘
-                    │
-                    ▼
-         ┌──────────────────────┐
-         │ GeoJSON Adapter      │
-         │ • Format geometry    │
-         │ • Embed properties   │
-         │ • Group by station   │
-         └──────────┬───────────┘
-                    │
-                    ▼
-        Response (GeoJSON)
-
-
-
-
-
-
-

Development Setup#

-
-

Prerequisites#

-
    -
  • Python 3.10+

  • -
  • pip or conda

  • -
  • Git

  • -
  • Docker (optional, for containerized development)

  • -
-
-
-

Local Installation#

-
# Clone repository
-git clone https://github.com/UW-Madison-DSI/ag_forecasting_api.git
-cd ag_forecasting_api
-
-# Create virtual environment
-python -m venv .venv
-source .venv/bin/activate  # On Windows: .venv\Scripts\activate
-
-# Install dependencies
-pip install -r requirements.txt
-
-# Set environment variables
-export IBM_API_KEY="your_key"
-export TENANT_ID="your_tenant"
-export ORG_ID="your_org"
-
-# Run development server
-uvicorn app:app --reload --host 0.0.0.0 --port 8000
-
-
-
-
-

API Documentation#

-

Once running, view interactive documentation:

- -
-
-

Testing#

-
# Syntax validation
-python -m py_compile api/models/risk_models.py api/services/*.py
-
-# Run linting (if installed)
-pylint api/
-
-# Test endpoint with curl
-curl "http://localhost:8000/v2/test/rye_biomass"
-
-
-
-
-
-
-

Deployment#

-
-

Docker Deployment#

-
FROM python:3.11-slim
-WORKDIR /app
-COPY requirements.txt .
-RUN pip install --no-cache-dir -r requirements.txt
-COPY . .
-EXPOSE 8000
-CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
-
-
-
-
-

Docker Compose (Production)#

-
version: "3.9"
-services:
-  ag_forecasting_api:
-    build: .
-    expose:
-      - 8000
-    environment:
-      - PYTHONPATH=/app
-      - ENVIRONMENT=production
-      - IBM_API_KEY=${IBM_API_KEY}
-      - TENANT_ID=${TENANT_ID}
-      - ORG_ID=${ORG_ID}
-    networks:
-      - traefik
-    labels:
-      - "traefik.enable=true"
-      - "traefik.http.routers.ag_api.rule=Host(`your-domain.com`)"
-      - "traefik.http.services.ag_api.loadbalancer.server.port=8000"
-    healthcheck:
-      test: ["CMD", "curl", "-f", "http://localhost:8000/"]
-      interval: 30s
-      timeout: 10s
-      retries: 3
-
-
-
-
-

Environment Variables#

-

Required for production:

-
    -
  • IBM_API_KEY: IBM EIS authentication credential

  • -
  • TENANT_ID: IBM EIS tenant identifier

  • -
  • ORG_ID: IBM EIS organization identifier

  • -
  • ENVIRONMENT: Set to “production” for optimized logging

  • -
-

Optional:

-
    -
  • PYTHONPATH: Should include /app for imports

  • -
  • LOG_LEVEL: Logging verbosity (default: INFO)

  • -
-
-
-
-
-

Performance Considerations#

-
-

Caching Strategy#

-
    -
  • Station Metadata: 7-day TTL (rarely changes)

  • -
  • Measurements: 6-hour TTL (updated daily by WiscoNet)

  • -
  • IBM Responses: Variable (tied to forecast date relevance)

  • -
  • File System: JSON cache in api_cache/ for persistence

  • -
-
-
-

Concurrency#

-
    -
  • Async Pipeline: Multi-station queries use async/await for I/O efficiency

  • -
  • Parallel Risk Scoring: Disease calculations use multiprocessing (4 workers default)

  • -
  • Connection Pooling: HTTP clients maintain connection pools (max 50 concurrent)

  • -
-
-
-

Optimization Tips#

-
    -
  1. Batch Requests: Query multiple stations in one call rather than sequential individual queries

  2. -
  3. Use WiscoNet: Public API has lower latency than premium IBM service

  4. -
  5. Limit Date Range: Smaller date ranges reduce computation time

  6. -
  7. Filter Diseases: Specify disease models needed to reduce calculation overhead

  8. -
  9. Cache Warming: Pre-fetch station lists during off-peak hours

  10. -
-
-
-
-
-

Additional Resources#

- -
-
-
-

License#

-

See LICENSE file for details.

-
-
-
-

Changelog#

-
-

Version 2.0 (Current)#

-
    -
  • ✅ Winter rye biomass model integration

  • -
  • ✅ Unified API versioning (v1 legacy, v2 current)

  • -
  • ✅ Enhanced GeoJSON responses

  • -
  • ✅ Improved async pipeline performance

  • -
  • ✅ Extended documentation

  • -
-
-
-

Version 1.0 (Legacy)#

-
    -
  • Disease risk models for corn and soybean

  • -
  • WiscoNet and IBM EIS integration

  • -
  • Basic GeoJSON support

  • -
-
-

Last Updated: May 2025
-Maintained By: UW-Madison Data Science Institute

-
-
-
- - - - -
- - - - - - - - -
- - - - - - -
-
- - -
- - -
-
-
- - - - - -
-
- - \ No newline at end of file diff --git a/_build/html/applications/ag-forecasting-api.html b/_build/html/applications/ag-forecasting-api.html index 8ecb7b3..bd326b1 100644 --- a/_build/html/applications/ag-forecasting-api.html +++ b/_build/html/applications/ag-forecasting-api.html @@ -32,7 +32,7 @@ - + @@ -60,6 +60,7 @@ + @@ -181,13 +182,11 @@ @@ -232,53 +231,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -435,6 +390,23 @@

An Application Case Study: Forecasting Crop Disease with OpenLambda

@@ -450,6 +422,105 @@

An Application Case Study: Forecasting Crop Disease with OpenLambdaOur goal is to make an ever-growing set of applications deployable on OpenLambda (OL), with minimal modifications. We believe the best way to work towards this goal is to pick interesting applications that weren’t originally designed for serverless deployment, try to port them to OL, and identify pain points. This helps us identify the most useful features to add to OL, to support similar deployments.

Recently, we selected an agricultural forecasting application (AgForecast), developed by the Data Science Institute at UW–Madison, to port to OpenLambda: UW-Madison-DSI/ag_forecasting_api. AgForecast is an interesting case study, because it implements its REST API using FastAPI, which in turn uses ASGI, the so-called “spiritual successor” to WSGI, which we recently started supporting in OpenLambda. WSGI is the basis for popular Python web-programming packages such as Django and Flask; new ASGI support opens the door to an even broader range of applications.

In this post, we describe the challenges of porting AgForecast to OL, and four new features we added to OL to make deployment of similar applications in the future simpler. The features are built-in ASGI support, direct GitHub-to-OL deployments, OL function environment variables, and OL-based pip compilation.

+
+

Background: Agricultural Forecasting API#

+

Motivation. The agricultural forecasting app is an open-source tool designed to address farmers’ needs in a customizable way. We first built a backend infrastructure using FastAPI to serve crop disease forecasting models for corn and soybean, focusing on integration with Wisconet weather stations and on-demand model serving. We then developed a custom R Shiny front-end interface for farmer use.

+

Fit with OpenLambda. We began exploring OpenLambda to leverage the benefits of serverless technology. Hosting our tool on this platform offers significant value not only to the developer community, but also to plant pathology practitioners and scientists who want to build on robust, nationwide infrastructure.

+

Technical overview. The Ag Forecasting API is a FastAPI-based backend that serves crop disease forecasting models for corn and soybean across Wisconsin. It exposes two parallel data pipelines through a unified interface: one sourced from Wisconet, the public mesonet of weather stations across the state, and another from IBM Environmental Intelligence for point-location queries by latitude and longitude. The core logic lives in the ag_models_wrappers module, which dynamically pulls the daily and hourly weather variables each disease model requires for a given forecasting date, runs the risk calculations, and returns localized predictions. Currently supported models include Sporecaster (white mold in soybean, with dry and irrigated row-spacing variants), Tarspotter (tar spot of corn), gray leaf spot, and frogeye leaf spot — all based on peer-reviewed plant pathology research from UW–Madison.

+

Infrastructure and integration. The project ships with a Dockerfile and docker-compose.yml for containerized deployment, and includes a Starlette WSGIMiddleware wrapper so the FastAPI app can also be served behind WSGI servers for legacy or mixed environments. A companion sub-package, pywisconet, provides a thin REST wrapper over the Wisconet v1 API with endpoints for active station discovery (/wisconet/active_stations/), station field metadata (/station_fields/{station_id}), and bulk measurement retrieval (/bulk_measures/{station_id}) at configurable frequencies (5-minute, hourly, or daily) for variables like air temperature, dew point, wind speed, and relative humidity. IBM credentials are handled via environment variables (IBM_API_KEY, TENANT_ID, ORG_ID) and validated against query parameters at request time. The API is MIT-licensed, fully open source, and currently deployed at connect.doit.wisc.edu/ag_forecasting_api.

+
+
+

Porting to OpenLambda#

+

When porting AgForecast to OL, we encountered 5 challenges related to: expectations about writable directories, package version selection, deployment from GitHub, asynchronous execution, and parallel pool execution. To overcome these challenges, we introduced four new features to OL and made minor changes to AgForecast itself.

+
+

Challenge 1: File Management#

+

AgForecast is semi-stateless: data files describing stations and measurements are used across requests, but if these files are deleted, it can generate them on-the-fly from upstream data sources, such as the IBM Weather API or Wisconet. This is a good match for FaaS platforms such as OL, where lambda instances frequently persist (along with their state) across multiple invocations, even if an instance can silently be terminated at any time to reclaim memory.

+

However, most directory locations are read-only for an OL function; the one exception is a single “scratch directory” and various other locations that are symbolic links to the scratch directory (such as /tmp). Full-featured sandboxes such as Docker containers use union file systems to make many directories editable, on a copy-on-write basis. OL’s limitation is due to its use of bind mounts, a leaner, but less flexible mechanism. AgForecast wasn’t originally built for OL, so we modified the code to make the directory location for station and measurement data configurable via environment variables (otherwise AgForecast attempted to write to read-only locations). We added support to OL function configuration files to support the specification of environment variables, like this:

+
triggers:
+  http:
+    - method: "*"
+environment:
+  MEASUREMENTS_CACHE_DIR: /host/tmp/cache
+  STATIONS_CACHE_FILE: /host/tmp/cache/wisconsin_stations_cache.csv
+  ...
+
+
+
+
+

Challenge 2: Package Management#

+

Like many Python projects, AgForecast specifies PyPI package requirements in a requirements.txt file. Also like most projects, not all version requirements are exact. Here are 3 of the 21 lines in AgForecast’s requirements.txt:

+
...
+matplotlib==3.9.1
+fastapi>=0.95.0
+pydantic
+...
+
+
+

Note the different levels of specificity: matplotlib must be a specific version, whereas pip can select any version for pydantic (probably the latest, barring version conflicts based on other dependencies); fastapi specifies a range. Note that some of these might also have indirect dependencies on other packages not explicitly listed.

+

OL requires exact versions for all packages, direct or indirect. For this purpose, we recommend the use of pip-compile to translate a partially specified requirements.txt file to a fully specified one, based on the latest packages at the time of compilation.

+

One challenge is that pip and pip-compile sometimes select packages based on the host environment. For example, in certain cases, the suitable Python package version for Ubuntu 24.04 might be different than for Ubuntu 26.04. Initially, we encountered this when deploying AgForecast on OL. The requirements.txt created by doing pip-compile on the host machine was not quite compatible with the environment inside the lambda function. To address this, we created a new OL function that does pip-compile inside the OL environment. It works like this:

+
curl -X POST -d '<some URL>' http://localhost:5000/run/pip-compile/url > requirements.txt
+
+
+
+
+

Challenge 3: Deployment#

+

AgForecast lives on a public GitHub repo: UW-Madison-DSI/ag_forecasting_api. We wanted to make it as easy as possible to deploy directly from GitHub to an OL function. Thus, we added a new ol admin install option to point directly to a repo:

+
./ol admin install -c ol.yaml -r requirements.txt https://github.com/<org>/<repo>.git
+
+
+
+
+

Challenge 4: Asynchronous Execution#

+

AgForecast is built on FastAPI, which in turn is based on ASGI. ASGI is an asynchronous alternative to WSGI (Web Server Gateway Interface). The idea of WSGI is to let you mix and match servers (for example, Gunicorn, uWSGI) with application frameworks (for example, Flask, Django). The server/framework interface is minimalist, a single function signature that the server calls for each incoming HTTP request (GET/POST/etc). The framework implements the function; a common framework pattern is to route the call to a user-written handler function. For example, consider these two functions:

+
app = Flask(__name__)
+
+@app.route("/")
+def home():
+    return "Home page"
+
+@app.route("/about")
+def about():
+    return "About page"
+
+
+

The app object is a Python callable (meaning it is an object that acts like a function); app implements the WSGI interface. So when a server sends a request to app, app in turn calls the correct user function (home or about) to execute and obtain a result.

+

ASGI was introduced as an alternative to WSGI to provide more options for handling concurrent calls. Consider how (in the above Flask/WSGI example) two different users may want to visit the home and about pages at the same time. Can we handle the requests concurrently?

+

There are a few ways to do this: multiple processes, multiple threads in a process, or Python’s async functionality introduced in Python 3.5 (2015). If we want all execution in a single process (useful when there is shared state), we can either use threading (with WSGI) or async (with ASGI).

+

Threads are a non-cooperative form of scheduling, meaning that a scheduler can switch from running one thread on a CPU to another thread at any time (perhaps a very inconvenient time!). Writing multi-threaded programs is notoriously difficult, as one must identify shared state, introduce locks to protect that state, and acquire/release locks at the right points. In contrast, async offers a form of cooperative scheduling, where switches can only occur at well-defined points (e.g., an await). Thus, programming is simpler (no need for locks).

+

Normally, concurrent programs allow (a) parallel execution on multiple CPU cores at the same time and (b) execution of code at the same time that input/output occurs, say to the disk or network. In many languages, threads offer both benefits and cooperative scheduling only provides the I/O benefit. However, Python threads only offer the I/O benefit due to the GIL (Global Interpreter Lock), though this may be changing. Thus, async is especially appealing in Python since it (in theory) matches threading in terms of performance benefits, and surpasses threads in terms of ease-of-programming.

+

AgForecast implements REST calls in FastAPI, which is based on ASGI, which is the async-based alternative to WSGI. To support AgForecast, we implemented ASGI server functionality in OpenLambda. This makes OpenLambda an ASGI server implementation (in the same role as Uvicorn, Gunicorn, Daphne, etc). When a request arrives, OpenLambda uses asyncio.run(...) to invoke the user-provided entry point, with async send/receive callbacks. The user-provided entry-point can then be a full application, written in any ASGI-compatible framework (FastAPI, Starlette, Django/Channels, etc).

+

A user can indicate their lambda function is an ASGI application entry point by configuring their ol.yaml as follows:

+
environment:
+  OL_ENTRY_FILE: app.py
+  OL_ASGI_ENTRY: app
+
+
+
+
+

Challenge 5: Worker Pools#

+

AgForecast indirectly uses /dev/shm, an in-memory file system called tmpfs. Docker containers have a /dev/shm mount by default; as it is frequently used for inter-process communication, its behavior is configurable via the --ipc flag. In contrast, OL functions do not have any /dev/shm mount.

+

Why does AgForecast need /dev/shm? AgForecast uses a concurrent.futures.ProcessPoolExecutor to run a Python compute-heavy function called compute_risks in parallel over different chunks of data. ProcessPoolExecutor creates different Python processes for different chunks of work, which is desirable as a way to get around Python’s per-process GIL (Global Interpreter Lock), which is held whenever regular Python code is being executed. With multiple processes (created by the pool), each process will have its own GIL that it holds during execution; thus, multiple processes can hold their own locks at the same time and execute on multiple CPU cores in parallel.

+

As a fix to get AgForecast working as an OL function, we replaced ProcessPoolExecutor with a ThreadPoolExecutor; this avoids inter-process communication via /dev/shm because coordination occurs within a process, between threads. Unfortunately, all the threads share a GIL, so the performance benefits of using a pool for parallelism is lost in this case (the only value would be if compute_risks were I/O heavy, which it is not).

+

Using a ThreadPoolExecutor works as a short term fix, but this experience suggests that eventually adding /dev/shm availability (and thus ProcessPoolExecutor functionality) would be a useful future feature for OL.

+
+
+
+

Recap of New Features#

+

Porting real, complex applications to serverless platforms highlights the most important features to develop. In this post, we described 5 challenges we encountered when porting AgForecast to OL, and the following features we added to better support similar applications:

+
    +
  • environment variable configuration for lambda functions (challenge 1)

  • +
  • pip-compile as a lambda function (challenge 2)

  • +
  • direct GitHub deployment (challenge 3)

  • +
  • ASGI support (challenge 4)

  • +
+

We also made some minor changes to AgForecast:

+
    +
  • customizable directory use for stations/measurements (challenge 1)

  • +
  • use of a thread pool instead of a process pool (challenge 5)

  • +
+

The second change suggests a future possible OL feature: adding /dev/shm availability to support process pools.

+
- - + @@ -330,53 +331,6 @@

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -