Skip to content

Commit 9509c99

Browse files
authored
Merge pull request #25 from VectorInstitute/add_automatic_workflow
Add automatic forecast workflow
2 parents 542e8d4 + 357dec5 commit 9509c99

35 files changed

+2675
-660
lines changed

.github/workflows/deploy-gcp.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -588,8 +588,9 @@ jobs:
588588
**Endpoints:**
589589
- Health: ${{ steps.deploy-backend.outputs.url }}/health
590590
- Model Info: ${{ steps.deploy-backend.outputs.url }}/model/info
591-
- Predict: ${{ steps.deploy-backend.outputs.url }}/predict
592-
- WebSocket: ${{ steps.deploy-backend.outputs.url }}/ws/predict
591+
- Latest Forecasts: ${{ steps.deploy-backend.outputs.url }}/forecasts/latest
592+
- Forecast Status: ${{ steps.deploy-backend.outputs.url }}/forecasts/status
593+
- Logs: ${{ steps.deploy-backend.outputs.url }}/logs/forecast-runs
593594
594595
### Frontend Service
595596
- **URL:** ${{ steps.deploy-frontend.outputs.url }}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,4 @@ TEST/
7171

7272
# predictions
7373
historical_predictions/
74+
forecasts/

README.md

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
# 🚨 GACA Early Warning System
1+
# Global AI Alliance for Climate Action
2+
3+
# High-Resolution Temperature Forecasting
24

35
----------------------------------------------------------------------------------------
46

@@ -11,9 +13,14 @@
1113

1214
### **NOAA URMA → Graph Neural Network (GCN-GRU) Temperature Forecasting**
1315

14-
This repository contains the **modular inference pipeline** for the **GACA Early Warning System**, a high-resolution Graph Neural Network framework that generates localized temperature forecasts from **NOAA URMA** meteorological data.
16+
Automated production forecasting system for Southwestern Ontario. Features hourly GCNGRU predictions stored in BigQuery with rolling 30-day evaluation metrics.
1517

16-
The pipeline takes the most recent URMA hourly fields, preprocesses them into graph-structured tensors, and produces **multi-horizon (1–48h)** temperature predictions across thousands of spatial nodes in Southwestern Ontario.
18+
**Key Features:**
19+
- **Automated Forecasts**: Hourly execution at :15 past each hour
20+
- **Multi-Horizon**: 1, 6, 12, 18, 24, 36, 48-hour predictions
21+
- **Live Dashboard**: Real-time forecast visualization with auto-refresh
22+
- **CLI Tools**: Manual inference and batch prediction capabilities
23+
- **Evaluation**: Daily rolling 30-day RMSE/MAE metrics
1724

1825
---
1926

@@ -44,6 +51,55 @@ source .venv/bin/activate
4451

4552
---
4653

54+
## Automated Forecasting System
55+
56+
The backend runs automated hourly forecasts and daily evaluations using APScheduler:
57+
58+
### Production Deployment
59+
60+
**Forecasting Schedule:**
61+
- Runs hourly at :15 (after NOAA data availability)
62+
- Automatically stores predictions to BigQuery
63+
- CSV outputs saved to configurable directory
64+
65+
**Evaluation Schedule:**
66+
- Runs daily at 00:30 UTC
67+
- Computes rolling 30-day metrics (RMSE/MAE)
68+
- SQL-based aggregation for efficiency
69+
70+
**Dashboard:**
71+
- Auto-refreshes every 30 minutes
72+
- Smart polling checks for new data before fetching
73+
- Displays scheduler status and last update time
74+
- Manual refresh button available
75+
76+
**Environment Variables:**
77+
```bash
78+
FORECAST_OUTPUT_DIR=forecasts/ # Output directory for CSVs
79+
GCP_PROJECT_ID=your-project-id # BigQuery project (optional)
80+
BIGQUERY_DATASET=gaca_evaluation # BigQuery dataset name
81+
```
82+
83+
### CLI Usage
84+
85+
The CLI remains unchanged and works independently of the automated system:
86+
87+
```bash
88+
# Single forecast
89+
gaca-ews predict --config config.yaml --output results/
90+
91+
# Batch predictions for date range
92+
gaca-ews batch-predict --start-date "2024-02-06 12:00" --end-date "2024-02-10 12:00" --interval 24
93+
94+
# Model information
95+
gaca-ews info
96+
97+
# Version
98+
gaca-ews version
99+
```
100+
101+
---
102+
47103
## Author
48104

49105
**Joud El-Shawa** - Vector Institute for AI & Western University

app/app/components/empty-state.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ export function EmptyState() {
99
<div className="text-center text-slate-400 bg-slate-900/80 backdrop-blur-md rounded-2xl p-8 border border-slate-700/50">
1010
<Cloud className="w-16 h-16 mx-auto mb-4 opacity-50" />
1111
<p className="mb-2 text-lg">No predictions loaded</p>
12-
<p className="text-sm">Click "Run Forecast" to generate predictions</p>
12+
<p className="text-sm">Forecasts run hourly at :15 past the hour</p>
13+
<p className="text-xs text-slate-500 mt-2">
14+
Dashboard will auto-refresh when data is available
15+
</p>
1316
</div>
1417
</div>
1518
);

app/app/components/empty-stats.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,16 @@ export function EmptyStats() {
77
return (
88
<div className="bg-slate-800/50 backdrop-blur-sm rounded-2xl border border-slate-700/50 p-12 text-center">
99
<Gauge className="w-16 h-16 text-slate-600 mx-auto mb-4" />
10-
<p className="text-slate-400">
11-
Statistics will appear here after running forecast
12-
</p>
10+
<div className="space-y-2">
11+
<p className="text-slate-300 font-medium">Waiting for forecast data</p>
12+
<p className="text-sm text-slate-400">
13+
Temperature statistics and visualizations will appear when the next hourly
14+
forecast completes
15+
</p>
16+
<p className="text-xs text-slate-500 mt-3">
17+
Forecasts run automatically every hour at :15
18+
</p>
19+
</div>
1320
</div>
1421
);
1522
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { Activity, Clock, RefreshCw } from "lucide-react";
2+
import type { ForecastStatus } from "../hooks/use-auto-predictions";
3+
4+
interface ForecastStatusProps {
5+
status: ForecastStatus | null;
6+
lastFetchTime: Date | null;
7+
onRefresh: () => void;
8+
loading: boolean;
9+
}
10+
11+
/**
12+
* Compact unified forecast status display with inline refresh
13+
*/
14+
export function ForecastStatusDisplay({
15+
status,
16+
lastFetchTime,
17+
onRefresh,
18+
loading,
19+
}: ForecastStatusProps) {
20+
const formatTime = (timestamp: string | null) => {
21+
if (!timestamp) return null;
22+
const date = new Date(timestamp);
23+
return date.toLocaleTimeString("en-US", {
24+
hour: "2-digit",
25+
minute: "2-digit",
26+
hour12: false,
27+
});
28+
};
29+
30+
const formatRelativeTime = (date: Date | null) => {
31+
if (!date) return null;
32+
const now = new Date();
33+
const diff = Math.floor((now.getTime() - date.getTime()) / 1000);
34+
35+
if (diff < 60) return `${diff}s`;
36+
if (diff < 3600) return `${Math.floor(diff / 60)}m`;
37+
return `${Math.floor(diff / 3600)}h`;
38+
};
39+
40+
const isRunning = status?.scheduler?.is_running || false;
41+
const nextRun = status?.scheduler?.next_scheduled_run;
42+
// Use scheduler timestamp first, fallback to last forecast from BigQuery
43+
const lastRun =
44+
status?.scheduler?.last_run_timestamp || status?.last_forecast?.run_timestamp;
45+
const relativeTime = formatRelativeTime(lastFetchTime);
46+
47+
return (
48+
<div className="absolute top-4 left-4">
49+
<div className="bg-slate-900/95 backdrop-blur-sm rounded-lg border border-slate-700/50 shadow-lg">
50+
<div className="flex items-center gap-3 px-3 py-2">
51+
{/* Status Icon & Text */}
52+
<div className="flex items-center gap-2">
53+
<Activity
54+
className={`w-3.5 h-3.5 ${isRunning ? "text-green-400 animate-pulse" : "text-blue-400"}`}
55+
/>
56+
<div className="text-xs text-white font-medium">
57+
{isRunning ? "Running" : "Auto"}
58+
</div>
59+
</div>
60+
61+
{/* Divider */}
62+
<div className="h-4 w-px bg-slate-700/50" />
63+
64+
{/* Time Info */}
65+
<div className="flex items-center gap-1.5 text-[10px] text-slate-400">
66+
{lastRun ? (
67+
<>
68+
<Clock className="w-3 h-3" />
69+
{relativeTime && <span>{relativeTime}</span>}
70+
{nextRun && !isRunning && (
71+
<>
72+
<span className="text-slate-600"></span>
73+
<span>Next {formatTime(nextRun)}</span>
74+
</>
75+
)}
76+
</>
77+
) : (
78+
<span>Waiting for first run</span>
79+
)}
80+
</div>
81+
82+
{/* Divider */}
83+
<div className="h-4 w-px bg-slate-700/50" />
84+
85+
{/* Refresh Button */}
86+
<button
87+
onClick={onRefresh}
88+
disabled={loading}
89+
className="hover:bg-slate-800/80 disabled:opacity-50 disabled:cursor-not-allowed rounded p-1 transition-all group"
90+
title="Refresh predictions"
91+
>
92+
<RefreshCw
93+
className={`w-3.5 h-3.5 text-blue-400 ${loading ? "animate-spin" : "group-hover:rotate-180 transition-transform duration-500"}`}
94+
/>
95+
</button>
96+
</div>
97+
</div>
98+
</div>
99+
);
100+
}

app/app/components/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
export { Header } from "./header";
22
export { CollapsedSidebar } from "./sidebar";
33
export { StatCard } from "./stat-card";
4-
export { MapControls } from "./map-controls";
4+
export { ForecastStatusDisplay } from "./forecast-status";
55
export { MapLegend } from "./map-legend";
66
export { ForecastInfo } from "./forecast-info";
77
export { HoverTooltip } from "./hover-tooltip";
8-
export { LoadingOverlay } from "./loading-overlay";
98
export { ErrorOverlay } from "./error-overlay";
109
export { EmptyState } from "./empty-state";
1110
export { TemperatureChart } from "./temperature-chart";
@@ -14,3 +13,5 @@ export { ModelInfoPanel } from "./model-info-panel";
1413
export { LoadingSkeleton } from "./loading-skeleton";
1514
export { EmptyStats } from "./empty-stats";
1615
export { HorizonSlider } from "./horizon-slider";
16+
export { LogTable } from "./log-table";
17+
export { LoadingOverlay } from "./loading-overlay";

0 commit comments

Comments
 (0)