+-----------+ +-----------+ +------------+
| CLI / | | REST API | | MCP Server |
| Scripts | | (FastAPI) | | (FastMCP) |
+-----+-----+ +-----+-----+ +-----+------+
| | |
v v v
+--------------------------------------------+
| agstack_pnd Core Library |
| |
| Layer 3: Disease/Pest Risk Models |
| FuzzyMamdani | RuleBased | PowderyMildew |
| | |
| Layer 2: Agronomic/Derived |
| LeafWetness | VPD | HumidityStreak | Phen|
| | |
| Layer 1: Weather/Accumulation |
| GDD | ChillHours | ChillPortions | Precip|
| | |
| Foundation |
| BaseModel ABC | WeatherProvider ABC |
| ModelRegistry | GeoResolver | Types |
+----+------+------+------+------------------+
| | | |
v v v v
+------+ +------+ +------+ +------+
| NOAA | |GeoID | | Threat | | DB |
| API | |Regis.| | Catalog| |(PG) |
+------+ +------+ +------+ +------+
src/agstack_pnd/
__init__.py # __version__
foundation/ # ABCs, types, registry
base_model.py # PestModelBase ABC
weather_provider.py # WeatherProvider ABC
geo_resolver.py # GeoID -> coordinates
model_registry.py # UUID catalog + entry-point discovery
types.py # WeatherDataFrame, ModelResult, ThreatDefinition, etc.
exceptions.py # Typed error hierarchy
models/
weather/ # Layer 1
growing_degree_days.py
chill_hours.py
chill_portions.py
accumulated_precip.py
agronomic/ # Layer 2
leaf_wetness_cart.py
leaf_wetness_rh.py
vpd.py
humidity_streak.py
phenology.py
disease/ # Layer 3
fuzzy_mamdani.py
rule_based.py
powdery_mildew_grape.py
providers/
noaa.py # NOAA CDO provider
catalog/
loader.py # Threat catalog loader
crops.py # Crop registry
threats.py # Threat accessors
data/pest_catalog.json # 54 crop-threat pairings
server/ # Optional service layer
app.py # FastAPI factory
config.py # Pydantic Settings
mcp_server.py # MCP tools + resources
routers/ # API endpoints
db/ # SQLAlchemy + Alembic
scheduler/ # Background NOAA sync
Every model inherits from PestModelBase. Key contract:
metadata: ClassVar[ModelMetadata]-- UUID, name, layer, supported crops, required fields, citationscalculate(weather_data, parameters, threat) -> ModelResult-- Pure computation, no I/Ovalidate_parameters(parameters) -> dict-- Input validation with defaults
get_hourly_data(lat, lon, start, end, fields) -> WeatherDataFrameget_daily_data(lat, lon, start, end, fields) -> WeatherDataFramesupported_fields() -> list[str]
Carries organism-specific biological parameters for disease models:
bio_params: t_base, t_lethal_min/max, t_optimal_min/max, min_streak, phenology windowsfuzzy_rules: humidity/temp/rainfall ranges per risk levelspecial_rules: organism-specific logic (Goidanich, Mills, two-fifteens, etc.)
UUID-based catalog with Python entry-point auto-discovery:
list_models(layer, crop)-- Discover modelsget_model(uuid)-- Instantiate by UUID- Third-party packages register via
[project.entry-points."agstack_pnd.models"]
- GDD (multi-base: 0/5/10C + custom)
- Chill Hours (Utah + 32-45)
- Chill Portions (Dynamic/Fishman)
- Accumulated Precipitation (3/7/10/14-day rolling)
- Leaf Wetness Duration (CART/SLD classifier)
- Leaf Wetness Estimation (RH/rainfall lookup)
- VPD (Tetens + rolling averages)
- Humidity Streak (consecutive days at RH thresholds)
- Phenological Gating (GDD-fraction trapezoidal membership)
- Fuzzy Mamdani (trapezoidal MFs, AND-aggregation, weighted defuzzification, gates)
- Rule-Based (boolean condition matching)
- Powdery Mildew Grape (UC IPM ascospore + conidial)
Disease models compose Layer 1 and Layer 2 outputs internally.
19 crops, 54 crop-threat pairings bundled as JSON seed data.
Each entry includes bio_params, fuzzy_rules, and special_rules.
Organism-specific special rules:
- Goidanich rule (Downy mildew): T>11C + RH>90% + rain>10mm
- Two-fifteens (Grey mold): 15C + 15h leaf wetness
- Mills table (Apple scab): temperature-dependent wetting periods
- Maryblyt (Fire blight): blossom blight risk model
- GDD flight windows (Codling moth): 250/468/900 GDD thresholds
- Wetness inhibition (Powdery mildew): free water inhibits infection
Standardized field names enforced by the ABC:
air_temperature(C),air_temperature_min(C),air_temperature_max(C)relative_humidity(%),dew_point(C),wind_speed(km/h)precipitation(mm),solar_radiation(W/m2),atmospheric_pressure(hPa)
NOAA CDO is the shipped provider. Community providers subclass WeatherProvider.
AgStack Asset Registry GeoIDs (SHA-256 hex, 64 chars) resolve to coordinates via:
GET https://api-ar.agstack.org/fetch-field-centroid/{geo_id}
Raw lat/lon always works as fallback.
GET /api/v1/models-- List models (filter by layer, crop)GET /api/v1/models/{uuid}-- Model metadataPOST /api/v1/calculate-- Run a modelGET /api/v1/crops-- Supported cropsGET /api/v1/crops/{crop}/threats-- Threats for a cropGET /health,GET /ready-- Probes
Tools: list_pest_models, get_model_info, get_supported_crops_tool, get_threats_for_crop_tool, explain_threat
Resources: pnd://models, pnd://crops
weather_cache-- NOAA response cache (TTL-based)model_runs-- Calculation audit logparcels-- Registered fields for weather pre-fetching
| Gate | Workflow | What it checks |
|---|---|---|
| Gate 1: Lint & Format | ci.yaml |
ruff check + ruff format |
| Gate 2: Type Check | ci.yaml |
mypy --strict on all source |
| Gate 3: Tests | ci.yaml |
pytest -- all unit and integration tests |
| Gate 4: Catalog & FATFD | ci.yaml |
Threat catalog schema, model UUID uniqueness, FATFD validator |
| Docs Build | ci.yaml |
sphinx-build (prevents doc regressions) |
- Disease models, agronomic models, threat catalog:
@agstack/openagri-maintainersmust approve - Foundation, providers, CI/CD:
@agstack/pnd-maintainersmust approve - Documentation: both teams
release.yaml: On tagv*-- build wheel, publish PyPI, push Docker to GHCRdocs.yaml: On push tomain/master-- deploy Sphinx to GitHub Pages
Adapted from the Full Harvest platform harness:
- Phases F through 9 (Feedback -> Audit -> Test & Fix -> ... -> Learn -> Write State)
- Domain-specific guardrails (G-DATA, G-MODEL, G-API, G-COMMUNITY)
- Enforcement rules E1-E15
- Validator checks A-K
.audit/directory with state, knowledge, manifest, and hooks
- Subclass
PestModelBase, implementcalculate()andvalidate_parameters() - Assign a UUID, fill in
ModelMetadata - Add entry point in
pyproject.toml - Write tests against
WeatherDataFramefixtures - Publish to PyPI -- model auto-discovers in every deployment