Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ copilot-instructions.md

IEM Data
*.parquet

#Run Output
/runs
5 changes: 5 additions & 0 deletions docs/dev-log/.authors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,9 @@ authors:
name: keenanjohnson
description: Creator
avatar: https://avatars.githubusercontent.com/u/2559382?v=4
anoushkagupta:
name: anoushkagupta
description: Contributor
avatar: https://avatars.githubusercontent.com/u/52936225?v=4


22 changes: 22 additions & 0 deletions docs/dev-log/posts/2026-06-28-starting-thoughts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
date: 2026-06-28
authors:
- anoushkagupta
---

# Introduction

Hey! I'm a Anoushka a new Engineer working on Loaf! As both a frequent "accidently stepped out in the rain" victim and a former Civil/Environmental Engineer, the value of better local weather predictions feels very clear and I'm excited to be dusting off some ML skills for this project!

## The Research
My initial thought on the research was how refreshingly accessible it was. The intent, reasoning, and procedure were clearly laid out and I was able to grasp the base logic within a read. I was suprised that they were able to get such an improvement in RMSE from just a transformer model, signaling the value in this work. I'm curious to how this research could apply to regions with more stable weather conditions than the Northeast (I'm from LA the land of constant perfect weather).

I also looked into what other literature was other on ML models localizing weather and found ClimaX by Microsoft and some more neural based models. I'd love to avoid a black box model and maintain understandibility as much as possible, but also am curious how other models perform.

## Next Steps
After diving into both the research and the code, I want to:
- **Recreate modeling with a local dataset** - I'm based in the DMV and am from LA and am curious how the model handels both area
- **Finish full training cycle** - close some gaps in the code regarding training/model assesment
- **Try other model types to drive accuracy** - based on other literature see how other models compare for same dataset
- **Make codebase more user-friendly** - the goal is to eventually make the model self-service and I have a few thoughts on how to make the UX as friendly as possible

8 changes: 4 additions & 4 deletions plan/dev-plan-ml-pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -555,10 +555,10 @@ class LOAFWeatherEntity(WeatherEntity):

### Milestone 3: Training Pipeline
- [x] Create PyTorch Dataset class (`loaf/data/loaders/dataset.py`)
- [ ] Implement training loop (`loaf/training/trainer.py`)
- [ ] Implement evaluation metrics (`loaf/training/evaluate.py`)
- [ ] Write `scripts/train.py` CLI
- [ ] **Verify:** Train on 1 month data, loss decreases, metrics improve
- [x] Implement training loop (`loaf/training/trainer.py`)
- [x] Implement evaluation metrics (`loaf/training/evaluate.py`)
- [x] Write `scripts/train.py` CLI
- [x] **Verify:** Train on downloaded Arlington data (MPNN and ViT), loss decreases, metrics improve

### Milestone 4: Inference & API
- [ ] Implement predictor (`loaf/inference/predictor.py`)
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ dependencies = [
# Utilities
"tqdm>=4.65.0",
"python-dotenv>=1.0.0",
"pyarrow>=14.0.0",
]

[project.optional-dependencies]
Expand Down
114 changes: 114 additions & 0 deletions software/config/arlington.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# LOAF Configuration for Arlington Region
# Based on LocalizedWeather paper methodology

region:
name: arlington
description: "Arlington and DMV Region"
# Spatial bounds (WGS84)
lat_min: 38.0
lat_max: 39.5
lon_min: -78
lon_max: -76

data:
# Historical window for input features
back_hrs: 24
# Forecast horizon
lead_hrs: 48
# Lead times to predict (hours from model init)
lead_times: [6, 12, 18, 24, 30, 36, 42, 48]

hrrr:
# Variables to download (GRIB2 search pattern)
# TMP:2 m - 2 meter temperature
# DPT:2 m - 2 meter dewpoint
# UGRD:10 m - 10 meter U wind component
# VGRD:10 m - 10 meter V wind component
variables: "(?:TMP:2 m|DPT:2 m|UGRD:10 m|VGRD:10 m)"
# Maximum forecast lead time to download per run
max_lead_hr: 18
# Output directory (relative to project root)
output_dir: "data/hrrr"

era5:
# ERA5 single-level variables
variables:
- "10m_u_component_of_wind"
- "10m_v_component_of_wind"
- "2m_temperature"
- "2m_dewpoint_temperature"
output_dir: "data/era5"

iem:
stations:
- "DCA" # Reagan National
- "IAD" # Dulles
- "BWI" # Baltimore-Washington
- "HEF" # Manassas Regional
- "MRB" # Eastern WV
# Iowa Environmental Mesonet - ASOS/AWOS stations
# No registration required, good for prototyping
# Variables: tmpc (temp C), dwpc (dewpoint C), sknt (wind knots), drct (wind dir)
variables:
- "tmpc"
- "dwpc"
- "sknt"
- "drct"
output_dir: "data/iem"
format: "parquet"

madis:
# MADIS station networks for DMV
# Requires registration: https://madis.ncep.noaa.gov/data_application.shtml
networks:
- "ASOS"
- "VDOT" # Virginia Department of Transportation
- "CWOP" # Citizen Weather Observer Program
- "RAWS" # Remote Automated Weather Stations
# Quality control - only use values with these flags
qc_flags: ["S", "V"] # S=Standard, V=Verified
# Maximum wind speed to include (filter outliers)
max_wind_speed: 50.0
output_dir: "data/madis"

model:
# Model architecture settings (match LocalizedWeather paper)
hidden_dim: 128
num_gnn_layers: 2
num_transformer_layers: 5
num_heads: 3
dropout: 0.1

# Graph construction
graph:
# K-nearest neighbors for station graph
k_neighbors: 5
# Maximum edge distance in km
max_distance: 100.0

training:
epochs: 100
batch_size: 64
learning_rate: 1.0e-4
weight_decay: 1.0e-4
# Fraction of data for validation
val_split: 0.15
# Early stopping patience (epochs)
patience: 10
# Gradient clipping
max_grad_norm: 1.0
# Random seed for reproducibility
seed: 42

inference:
# Output directory for predictions
output_dir: "data/forecasts"
# Update interval for operational mode (seconds)
update_interval: 3600 # 1 hour

homeassistant:
# REST API settings
host: "0.0.0.0"
port: 5000
# Sensor update interval (seconds)
scan_interval: 3600
88 changes: 66 additions & 22 deletions software/loaf/data/download/era5.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,46 +234,71 @@ def load_era5_month(file_path: str | Path) -> xr.Dataset:
return ds


def _load_era5_settings_from_config(config_path: str) -> dict:
"""Extract ERA5-relevant settings from a LOAF config file."""
from loaf.config import load_config

cfg = load_config(config_path)
era5_cfg = cfg.get("data", {}).get("era5", {})
region_cfg = cfg.get("region", {})
return {
"variables": era5_cfg.get("variables"),
"output_dir": era5_cfg.get("output_dir"),
"lat_min": region_cfg.get("lat_min"),
"lat_max": region_cfg.get("lat_max"),
"lon_min": region_cfg.get("lon_min"),
"lon_max": region_cfg.get("lon_max"),
}


def main() -> None:
"""CLI entry point for ERA5 download."""
parser = argparse.ArgumentParser(
description="Download ERA5 reanalysis data for a specified region and time range."
)

parser.add_argument(
"--config",
"-c",
default=None,
help="Path to a LOAF YAML config file (e.g. config/arlington.yaml). "
"Provides region bounds, variables, and output settings. "
"CLI flags override config values when both are provided.",
)
parser.add_argument(
"--output-dir",
"-o",
default="data/era5",
default=None,
help="Output directory for NetCDF files (default: data/era5)",
)
parser.add_argument(
"--variables",
nargs="+",
default=DEFAULT_VARIABLES,
default=None,
help="ERA5 variables to download",
)
parser.add_argument(
"--lat-min",
type=float,
default=SEATTLE_BOUNDS["lat_min"],
default=None,
help="Minimum latitude (default: 46.5 for Seattle)",
)
parser.add_argument(
"--lat-max",
type=float,
default=SEATTLE_BOUNDS["lat_max"],
default=None,
help="Maximum latitude (default: 49.0 for Seattle)",
)
parser.add_argument(
"--lon-min",
type=float,
default=SEATTLE_BOUNDS["lon_min"],
default=None,
help="Minimum longitude (default: -124.0 for Seattle)",
)
parser.add_argument(
"--lon-max",
type=float,
default=SEATTLE_BOUNDS["lon_max"],
default=None,
help="Maximum longitude (default: -121.0 for Seattle)",
)
parser.add_argument(
Expand Down Expand Up @@ -317,7 +342,26 @@ def main() -> None:
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)

output_dir = Path(args.output_dir)
# Load config defaults, then let CLI args override
cfg_settings: dict = {}
if args.config:
cfg_settings = _load_era5_settings_from_config(args.config)
logger.info(f"Loaded config from {args.config}")

output_dir = Path(args.output_dir or cfg_settings.get("output_dir") or "data/era5")
variables = args.variables or cfg_settings.get("variables") or DEFAULT_VARIABLES
lat_min = args.lat_min if args.lat_min is not None else cfg_settings.get(
"lat_min", SEATTLE_BOUNDS["lat_min"]
)
lat_max = args.lat_max if args.lat_max is not None else cfg_settings.get(
"lat_max", SEATTLE_BOUNDS["lat_max"]
)
lon_min = args.lon_min if args.lon_min is not None else cfg_settings.get(
"lon_min", SEATTLE_BOUNDS["lon_min"]
)
lon_max = args.lon_max if args.lon_max is not None else cfg_settings.get(
"lon_max", SEATTLE_BOUNDS["lon_max"]
)

if args.year and args.month:
# Single month download
Expand All @@ -326,23 +370,23 @@ def main() -> None:
args.year,
args.month,
filename,
args.variables,
args.lat_min,
args.lat_max,
args.lon_min,
args.lon_max,
variables,
lat_min,
lat_max,
lon_min,
lon_max,
)

elif args.year:
# Full year download
download_era5_year(
args.year,
output_dir,
args.variables,
args.lat_min,
args.lat_max,
args.lon_min,
args.lon_max,
variables,
lat_min,
lat_max,
lon_min,
lon_max,
)

elif args.start_year and args.end_year:
Expand All @@ -353,11 +397,11 @@ def main() -> None:
args.end_year,
args.end_month,
output_dir,
args.variables,
args.lat_min,
args.lat_max,
args.lon_min,
args.lon_max,
variables,
lat_min,
lat_max,
lon_min,
lon_max,
)

else:
Expand Down
Loading
Loading