mahwizzzz commited on
Commit
ba8c095
·
verified ·
1 Parent(s): 6c9db43

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +166 -0
app.py ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import torch
3
+ import numpy as np
4
+ import pandas as pd
5
+ import plotly.graph_objects as go
6
+ from transformers import TimesFm2_5ModelForPrediction
7
+ from datetime import datetime, timedelta
8
+ import tempfile
9
+ import os
10
+
11
+ MODEL_ID = "mahwizzzz/skyloom"
12
+ CONTEXT_LEN = 1024
13
+ MAX_HORIZON = 72
14
+
15
+ print("Loading model from Hub...")
16
+ device = "cuda" if torch.cuda.is_available() else "cpu"
17
+ model = TimesFm2_5ModelForPrediction.from_pretrained(
18
+ MODEL_ID,
19
+ torch_dtype=torch.bfloat16 if device == "cuda" else torch.float32,
20
+ device_map=device if device == "cuda" else None
21
+ )
22
+ model.eval()
23
+ print("Model ready")
24
+
25
+ def forecast_from_array(past_values: np.ndarray, horizon: int = 24) -> np.ndarray:
26
+ if len(past_values) < CONTEXT_LEN:
27
+ pad_len = CONTEXT_LEN - len(past_values)
28
+ past_values = np.pad(past_values, (pad_len, 0), mode='edge')
29
+ else:
30
+ past_values = past_values[-CONTEXT_LEN:]
31
+
32
+ input_tensor = torch.tensor(past_values[None, :], dtype=torch.float32).to(device)
33
+ with torch.no_grad():
34
+ output = model(past_values=input_tensor, forecast_context_len=CONTEXT_LEN)
35
+ forecast = output.mean_predictions[0, :horizon].float().cpu().numpy()
36
+ return np.clip(forecast, 0, None)
37
+
38
+ def load_csv(file):
39
+ df = pd.read_csv(file.name)
40
+ if 'aqi' in df.columns:
41
+ values = df['aqi'].values
42
+ else:
43
+ values = df.iloc[:, 0].values
44
+ return values.astype(float)
45
+
46
+ def forecast_from_csv(file, horizon):
47
+ values = load_csv(file)
48
+ if len(values) < 2:
49
+ raise gr.Error("CSV must contain at least 2 AQI values.")
50
+ forecast = forecast_from_array(values, horizon)
51
+ return create_plot(values[-CONTEXT_LEN:], forecast, horizon)
52
+
53
+ def forecast_from_text(text, horizon):
54
+ try:
55
+ values = np.array([float(x.strip()) for x in text.split(',') if x.strip()])
56
+ except:
57
+ raise gr.Error("Please enter a comma-separated list of numbers (e.g., 45.2, 47.1, 46.8)")
58
+ if len(values) < 2:
59
+ raise gr.Error("At least 2 values required.")
60
+ forecast = forecast_from_array(values, horizon)
61
+ return create_plot(values[-CONTEXT_LEN:], forecast, horizon)
62
+
63
+ def create_plot(history, forecast, horizon):
64
+ hist_indices = list(range(-len(history), 0))
65
+ fore_indices = list(range(1, horizon+1))
66
+
67
+ fig = go.Figure()
68
+ fig.add_trace(go.Scatter(
69
+ x=hist_indices, y=history,
70
+ mode='lines+markers',
71
+ name='Past AQI',
72
+ line=dict(color='#1f77b4', width=2),
73
+ marker=dict(size=4)
74
+ ))
75
+ fig.add_trace(go.Scatter(
76
+ x=fore_indices, y=forecast,
77
+ mode='lines+markers',
78
+ name='Forecast',
79
+ line=dict(color='#ff7f0e', width=3, dash='dot'),
80
+ marker=dict(size=6, symbol='triangle-up')
81
+ ))
82
+
83
+ fig.add_vline(x=0, line_dash="dash", line_color="gray", opacity=0.5)
84
+ fig.add_annotation(x=0, y=max(history[-10:]+forecast[:5]), text="Forecast start", showarrow=False, yshift=10)
85
+
86
+ fig.update_layout(
87
+ title=f"AQI Forecast – Next {horizon} hours",
88
+ xaxis_title="Hours relative to present (0 = current hour)",
89
+ yaxis_title="Air Quality Index (AQI)",
90
+ template="plotly_white",
91
+ hovermode="x unified",
92
+ legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1)
93
+ )
94
+ return fig
95
+
96
+ with gr.Blocks(theme=gr.themes.Soft(), title="Skyloom – Karachi AQI Forecaster", css="footer {visibility: hidden}") as demo:
97
+ gr.Markdown("""
98
+ # Skyloom: AQI Forecaster for Karachi
99
+ **TimesFM‑2.5** – Fine‑tuned for hourly Air Quality Index predictions.
100
+ """)
101
+
102
+ with gr.Row():
103
+ with gr.Column(scale=1):
104
+ gr.Markdown("### Input Data")
105
+ input_method = gr.Radio(["Paste values", "Upload CSV"], label="Input method", value="Paste values")
106
+
107
+ with gr.Group(visible=True) as text_group:
108
+ text_input = gr.Textbox(
109
+ label="Past AQI values (comma‑separated, at least 24 values, oldest first)",
110
+ placeholder="45.2, 47.1, 46.8, 48.3, ...",
111
+ lines=3
112
+ )
113
+
114
+ with gr.Group(visible=False) as csv_group:
115
+ csv_input = gr.File(label="Upload CSV", file_types=[".csv"])
116
+ gr.Markdown("CSV should have a column named `aqi` or the first column containing hourly AQI values.")
117
+
118
+ horizon_slider = gr.Slider(label="Forecast horizon (hours)", minimum=1, maximum=72, value=24, step=1)
119
+
120
+ forecast_btn = gr.Button("Generate Forecast", variant="primary")
121
+
122
+ with gr.Column(scale=2):
123
+ plot_output = gr.Plot(label="Forecast Chart")
124
+ gr.Markdown("""
125
+ **Interpretation**
126
+ - **Past AQI** (blue): last 1024 hours of history (or the portion you provided)
127
+ - **Forecast** (orange dashed): predicted AQI for the next hours
128
+ - AQI categories:
129
+ - 0–50 Good
130
+ - 51–100 Moderate
131
+ - 101–150 Unhealthy for Sensitive Groups
132
+ - 151–200 Unhealthy
133
+ - 201+ Very Unhealthy / Hazardous
134
+ """)
135
+
136
+ def toggle_visibility(method):
137
+ return gr.update(visible=(method == "Paste values")), gr.update(visible=(method == "Upload CSV"))
138
+ input_method.change(toggle_visibility, inputs=input_method, outputs=[text_group, csv_group])
139
+
140
+ def on_text_forecast(text, horizon):
141
+ if not text.strip():
142
+ raise gr.Error("Please enter comma-separated AQI values.")
143
+ return forecast_from_text(text, horizon)
144
+
145
+ def on_csv_forecast(file, horizon):
146
+ if file is None:
147
+ raise gr.Error("Please upload a CSV file.")
148
+ return forecast_from_csv(file, horizon)
149
+
150
+ forecast_btn.click(
151
+ fn=lambda method, text, csv, horizon:
152
+ forecast_from_text(text, horizon) if method == "Paste values" else forecast_from_csv(csv, horizon),
153
+ inputs=[input_method, text_input, csv_input, horizon_slider],
154
+ outputs=plot_output
155
+ )
156
+
157
+ gr.Markdown("""
158
+ ---
159
+ **Model info**
160
+ - Base model: `google/timesfm-2.5-200m-transformers`
161
+ - Test MAE: **5.18** | sMAPE: **5.46%** | MASE: **0.47**
162
+ - *Disclaimer*: Forecasts are for informational purposes only. Sudden pollution events may not be captured.
163
+ """)
164
+
165
+ if __name__ == "__main__":
166
+ demo.launch()