Aniket2006 commited on
Commit
1e44df5
Β·
0 Parent(s):

v1.0.0 TimeSeries Service with AutoNHITS

Browse files
Files changed (7) hide show
  1. Dockerfile +25 -0
  2. README.md +57 -0
  3. app.py +276 -0
  4. auto_tuning_predictor.py +441 -0
  5. prediction_pipeline.py +145 -0
  6. requirements.txt +17 -0
  7. satellite_pipeline.py +311 -0
Dockerfile ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10
2
+
3
+ WORKDIR /app
4
+
5
+ # Install system dependencies
6
+ RUN apt-get update && apt-get install -y \
7
+ gcc \
8
+ libgdal-dev \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ # Copy requirements first for caching
12
+ COPY requirements.txt .
13
+ RUN pip install --no-cache-dir -r requirements.txt
14
+
15
+ # Copy application files
16
+ COPY app.py .
17
+ COPY satellite_pipeline.py .
18
+ COPY auto_tuning_predictor.py .
19
+ COPY prediction_pipeline.py .
20
+
21
+ # Expose port
22
+ EXPOSE 7860
23
+
24
+ # Run the application
25
+ CMD ["python", "app.py"]
README.md ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: AGROW TimeSeries
3
+ emoji: πŸ“ˆ
4
+ colorFrom: green
5
+ colorTo: blue
6
+ sdk: docker
7
+ pinned: false
8
+ license: mit
9
+ ---
10
+
11
+ # AGROW Time Series Service
12
+
13
+ Time series forecasting for satellite indices using AutoNHITS.
14
+
15
+ ## Features
16
+
17
+ - Historical data from Sentinel-1 (SAR) and Sentinel-2 (Optical)
18
+ - AutoNHITS forecasting with weather integration
19
+ - Returns JSON data for interactive Flutter charts
20
+
21
+ ## API Endpoints
22
+
23
+ ### POST /timeseries
24
+
25
+ Generate time series with historical data and forecasts.
26
+
27
+ **Request:**
28
+ ```json
29
+ {
30
+ "center_lat": 26.1885,
31
+ "center_lon": 91.6894,
32
+ "field_size_hectares": 10.0,
33
+ "metric": "VV"
34
+ }
35
+ ```
36
+
37
+ **Response:**
38
+ ```json
39
+ {
40
+ "success": true,
41
+ "metric": "VV",
42
+ "historical": [{"date": "2024-01-01", "value": -12.5}, ...],
43
+ "forecast": [{"date": "2024-12-01", "value": -11.8, "confidence_low": -12.5, "confidence_high": -11.0}],
44
+ "trend": "improving",
45
+ "stats": {"min": -15.2, "max": -8.5, "mean": -11.5}
46
+ }
47
+ ```
48
+
49
+ ## Supported Metrics
50
+
51
+ - **SAR:** VV, VH (Sentinel-1)
52
+ - **Optical:** B02-B12 (Sentinel-2)
53
+
54
+ ## Required Secrets
55
+
56
+ - `SH_CLIENT_ID`
57
+ - `SH_CLIENT_SECRET`
app.py ADDED
@@ -0,0 +1,276 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ AGROW Time Series Service
3
+ =========================
4
+ Time series forecasting for satellite indices using AutoNHITS.
5
+
6
+ Features:
7
+ - Historical data from Sentinel-1 (SAR) and Sentinel-2 (Optical)
8
+ - AutoNHITS forecasting with weather integration
9
+ - Returns JSON data points for interactive Flutter charts
10
+ """
11
+
12
+ import os
13
+ import io
14
+ import json
15
+ import base64
16
+ import logging
17
+ import traceback
18
+ from datetime import datetime, timedelta
19
+ from typing import List, Optional, Dict, Any
20
+
21
+ import numpy as np
22
+ import pandas as pd
23
+ from fastapi import FastAPI, HTTPException
24
+ from fastapi.middleware.cors import CORSMiddleware
25
+ from pydantic import BaseModel
26
+
27
+ # Import our modules
28
+ from satellite_pipeline import SatelliteFetcher
29
+ from auto_tuning_predictor import AutoTimeSeriesPredictor
30
+
31
+ # ============================================================================
32
+ # LOGGING
33
+ # ============================================================================
34
+ logging.basicConfig(
35
+ level=logging.INFO,
36
+ format='[%(asctime)s] %(levelname)s: %(message)s',
37
+ datefmt='%H:%M:%S'
38
+ )
39
+ logger = logging.getLogger("TimeSeriesService")
40
+
41
+ # ============================================================================
42
+ # FASTAPI
43
+ # ============================================================================
44
+ app = FastAPI(
45
+ title="AGROW Time Series Service",
46
+ description="Time series forecasting for satellite indices",
47
+ version="1.0.0"
48
+ )
49
+
50
+ app.add_middleware(
51
+ CORSMiddleware,
52
+ allow_origins=["*"],
53
+ allow_credentials=True,
54
+ allow_methods=["*"],
55
+ allow_headers=["*"],
56
+ )
57
+
58
+ # ============================================================================
59
+ # REQUEST/RESPONSE MODELS
60
+ # ============================================================================
61
+ class DataPoint(BaseModel):
62
+ date: str
63
+ value: float
64
+
65
+ class ForecastPoint(BaseModel):
66
+ date: str
67
+ value: float
68
+ confidence_low: Optional[float] = None
69
+ confidence_high: Optional[float] = None
70
+
71
+ class TimeSeriesRequest(BaseModel):
72
+ center_lat: float
73
+ center_lon: float
74
+ field_size_hectares: float = 10.0
75
+ metric: str = "VV" # VV, VH, B04, B08, NDVI, etc.
76
+ days_history: int = 365 # How many days of history
77
+ days_forecast: int = 30 # How many days to forecast
78
+
79
+ class TimeSeriesResponse(BaseModel):
80
+ success: bool
81
+ metric: str
82
+ historical: List[DataPoint]
83
+ forecast: List[ForecastPoint]
84
+ trend: str # "improving", "stable", "declining"
85
+ stats: Dict[str, float]
86
+ timestamp: str
87
+
88
+ # ============================================================================
89
+ # HELPERS
90
+ # ============================================================================
91
+ def coords_to_polygon(center_lat: float, center_lon: float, size_ha: float):
92
+ """Convert center point and size to polygon coordinates."""
93
+ # Approximate conversion: 1 hectare = 0.01 kmΒ²
94
+ radius_km = np.sqrt(size_ha / 100) / 2
95
+ lat_off = radius_km / 111
96
+ lon_off = radius_km / (111 * np.cos(np.radians(center_lat)))
97
+
98
+ return [
99
+ (center_lon - lon_off, center_lat - lat_off),
100
+ (center_lon + lon_off, center_lat - lat_off),
101
+ (center_lon + lon_off, center_lat + lat_off),
102
+ (center_lon - lon_off, center_lat + lat_off),
103
+ (center_lon - lon_off, center_lat - lat_off), # Close polygon
104
+ ]
105
+
106
+ def calculate_trend(values: List[float]) -> str:
107
+ """Determine trend from values."""
108
+ if len(values) < 5:
109
+ return "stable"
110
+
111
+ # Simple linear regression slope
112
+ x = np.arange(len(values))
113
+ slope = np.polyfit(x, values, 1)[0]
114
+
115
+ if slope > 0.01:
116
+ return "improving"
117
+ elif slope < -0.01:
118
+ return "declining"
119
+ return "stable"
120
+
121
+ # ============================================================================
122
+ # API ENDPOINTS
123
+ # ============================================================================
124
+ @app.get("/")
125
+ async def root():
126
+ return {
127
+ "service": "AGROW Time Series Service",
128
+ "version": "1.0.0",
129
+ "metrics": ["VV", "VH", "B02", "B03", "B04", "B08", "B8A", "B11", "B12"],
130
+ "description": "Time series forecasting with AutoNHITS"
131
+ }
132
+
133
+ @app.get("/health")
134
+ async def health():
135
+ return {"status": "healthy"}
136
+
137
+
138
+ @app.post("/timeseries", response_model=TimeSeriesResponse)
139
+ async def get_timeseries(request: TimeSeriesRequest):
140
+ """
141
+ Fetch historical satellite data and generate forecasts.
142
+
143
+ This uses the full AutoNHITS model for accurate predictions.
144
+ Note: First request may take 2-5 minutes for data fetching and model training.
145
+ """
146
+ req_id = datetime.now().strftime("%H%M%S")
147
+ logger.info(f"[{req_id}] Time Series Request: {request.metric}")
148
+ logger.info(f"[{req_id}] Location: ({request.center_lat}, {request.center_lon})")
149
+
150
+ try:
151
+ # Step 1: Create polygon from center point
152
+ polygon = coords_to_polygon(
153
+ request.center_lat,
154
+ request.center_lon,
155
+ request.field_size_hectares
156
+ )
157
+ logger.info(f"[{req_id}] Polygon created: {polygon[:2]}...")
158
+
159
+ # Step 2: Fetch historical satellite data
160
+ logger.info(f"[{req_id}] Fetching satellite data...")
161
+ fetcher = SatelliteFetcher(polygon)
162
+
163
+ # Determine which data to fetch based on metric
164
+ if request.metric in ['VV', 'VH']:
165
+ fetcher.fetch_sar_data('sar_data.csv')
166
+ csv_file = 'sar_data.csv'
167
+ target_col = f'{request.metric}_mean_dB'
168
+ else:
169
+ fetcher.fetch_sentinel2_data('sentinel2_data.csv')
170
+ csv_file = 'sentinel2_data.csv'
171
+ target_col = request.metric
172
+
173
+ # Read historical data
174
+ if not os.path.exists(csv_file):
175
+ raise HTTPException(404, "No satellite data available for this location")
176
+
177
+ df = pd.read_csv(csv_file)
178
+ if 'ds' not in df.columns:
179
+ raise HTTPException(500, "Invalid data format")
180
+
181
+ # Convert to DataPoint list
182
+ historical = []
183
+ for _, row in df.iterrows():
184
+ if target_col in row and not pd.isna(row[target_col]):
185
+ historical.append(DataPoint(
186
+ date=str(row['ds']),
187
+ value=round(float(row[target_col]), 4)
188
+ ))
189
+
190
+ if len(historical) < 10:
191
+ raise HTTPException(400, f"Insufficient data points ({len(historical)}) for forecasting")
192
+
193
+ logger.info(f"[{req_id}] Historical data: {len(historical)} points")
194
+
195
+ # Step 3: Run AutoNHITS prediction
196
+ logger.info(f"[{req_id}] Running AutoNHITS prediction...")
197
+ predictor = AutoTimeSeriesPredictor()
198
+
199
+ predictions = predictor.tune_and_predict(
200
+ csv_path=csv_file,
201
+ field_coords=polygon,
202
+ target_col=target_col,
203
+ output_file='predictions.csv',
204
+ num_samples=5 # Quick tuning for API
205
+ )
206
+
207
+ # Convert predictions to ForecastPoint list
208
+ forecast = []
209
+ for _, row in predictions.iterrows():
210
+ value = float(row['predicted_y'])
211
+ forecast.append(ForecastPoint(
212
+ date=str(row['ds'].date()) if hasattr(row['ds'], 'date') else str(row['ds']),
213
+ value=round(value, 4),
214
+ confidence_low=round(value * 0.9, 4), # Simple confidence band
215
+ confidence_high=round(value * 1.1, 4)
216
+ ))
217
+
218
+ logger.info(f"[{req_id}] Forecast: {len(forecast)} points")
219
+
220
+ # Calculate stats
221
+ all_values = [p.value for p in historical]
222
+ stats = {
223
+ "min": round(min(all_values), 4),
224
+ "max": round(max(all_values), 4),
225
+ "mean": round(sum(all_values) / len(all_values), 4),
226
+ "count": len(all_values),
227
+ "forecast_count": len(forecast)
228
+ }
229
+
230
+ trend = calculate_trend(all_values[-20:] if len(all_values) > 20 else all_values)
231
+
232
+ # Cleanup temp files
233
+ for f in ['sar_data.csv', 'sentinel2_data.csv', 'predictions.csv']:
234
+ if os.path.exists(f):
235
+ os.remove(f)
236
+
237
+ logger.info(f"[{req_id}] SUCCESS - Trend: {trend}")
238
+
239
+ return TimeSeriesResponse(
240
+ success=True,
241
+ metric=request.metric,
242
+ historical=historical,
243
+ forecast=forecast,
244
+ trend=trend,
245
+ stats=stats,
246
+ timestamp=datetime.now().isoformat()
247
+ )
248
+
249
+ except HTTPException:
250
+ raise
251
+ except Exception as e:
252
+ logger.error(f"[{req_id}] ERROR: {str(e)}")
253
+ logger.error(traceback.format_exc())
254
+ raise HTTPException(500, str(e))
255
+
256
+
257
+ @app.get("/timeseries/quick")
258
+ async def quick_timeseries(
259
+ center_lat: float,
260
+ center_lon: float,
261
+ metric: str = "VV",
262
+ field_size_hectares: float = 10.0
263
+ ):
264
+ """Quick endpoint for simple GET requests."""
265
+ request = TimeSeriesRequest(
266
+ center_lat=center_lat,
267
+ center_lon=center_lon,
268
+ field_size_hectares=field_size_hectares,
269
+ metric=metric
270
+ )
271
+ return await get_timeseries(request)
272
+
273
+
274
+ if __name__ == "__main__":
275
+ import uvicorn
276
+ uvicorn.run(app, host="0.0.0.0", port=7860)
auto_tuning_predictor.py ADDED
@@ -0,0 +1,441 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import numpy as np
3
+ from shapely.geometry import Polygon
4
+ from sklearn.preprocessing import StandardScaler
5
+ import matplotlib.pyplot as plt
6
+
7
+ from neuralforecast import NeuralForecast
8
+ from neuralforecast.auto import AutoNHITS
9
+ from neuralforecast.losses.pytorch import MAE
10
+
11
+ import openmeteo_requests
12
+ import requests_cache
13
+ from retry_requests import retry
14
+ from openmeteo_sdk.Variable import Variable
15
+ from openmeteo_sdk.Aggregation import Aggregation
16
+ import warnings
17
+ import os
18
+ import datetime
19
+
20
+ # Suppress warnings
21
+ warnings.filterwarnings("ignore")
22
+
23
+ class AutoTimeSeriesPredictor:
24
+ def __init__(self):
25
+ # Coordinates will be set in tune_and_predict
26
+ self.field_coords = None
27
+ self.latitude = None
28
+ self.longitude = None
29
+
30
+ # OpenMeteo Client Setup
31
+ cache_session = requests_cache.CachedSession(".cache", expire_after=-1)
32
+ retry_session = retry(cache_session, retries=5, backoff_factor=0.2)
33
+ self.openmeteo = openmeteo_requests.Client(session=retry_session)
34
+
35
+ # Feature Configuration
36
+ self.hist_exog_list = ["lag1", "diff"]
37
+ self.futr_exog_list = ["temp", "rainfall", "humidity"]
38
+
39
+ # Scalers
40
+ self.y_scaler = StandardScaler()
41
+ self.exog_scaler = StandardScaler()
42
+
43
+ def set_coordinates(self, coords):
44
+ """
45
+ Sets the field coordinates and calculates the centroid latitude and longitude.
46
+ """
47
+ self.field_coords = coords
48
+ field_polygon = Polygon(self.field_coords)
49
+ self.latitude = field_polygon.centroid.y
50
+ self.longitude = field_polygon.centroid.x
51
+
52
+ def fetch_weather_data(self, start_date, end_date):
53
+ """
54
+ Fetches weather data, automatically switching between Historical and Ensemble Forecast APIs.
55
+ """
56
+ # Ensure dates are date objects
57
+ if isinstance(start_date, pd.Timestamp):
58
+ start_date = start_date.date()
59
+ if isinstance(end_date, pd.Timestamp):
60
+ end_date = end_date.date()
61
+
62
+ today = datetime.date.today()
63
+
64
+ dfs = []
65
+
66
+ # 1. Historical Data (if start_date < today)
67
+ if start_date < today:
68
+ hist_end = min(end_date, today - datetime.timedelta(days=1))
69
+ if start_date <= hist_end:
70
+ print(f"Fetching historical data from {start_date} to {hist_end}...")
71
+ try:
72
+ hist_df = self._fetch_historical_api(start_date, hist_end)
73
+ dfs.append(hist_df)
74
+ except Exception as e:
75
+ print(f"Error fetching historical data: {e}")
76
+
77
+ # 2. Forecast Data (if end_date >= today)
78
+ if end_date >= today:
79
+ print(f"Fetching forecast data from {today} to {end_date}...")
80
+ try:
81
+ # Calculate needed forecast days
82
+ days_needed = (end_date - today).days + 1
83
+ # API supports up to 35 days for ensemble
84
+ forecast_days = min(max(days_needed, 1), 35)
85
+
86
+ forecast_df = self._fetch_ensemble_forecast_api(forecast_days)
87
+
88
+ # Filter for requested range
89
+ forecast_df = forecast_df[
90
+ (forecast_df["ds"].dt.date >= today) &
91
+ (forecast_df["ds"].dt.date <= end_date)
92
+ ]
93
+ dfs.append(forecast_df)
94
+ except Exception as e:
95
+ print(f"Error fetching forecast data: {e}")
96
+
97
+ if not dfs:
98
+ print("Warning: No weather data fetched.")
99
+ return pd.DataFrame(columns=["ds", "temp", "humidity", "rainfall"])
100
+
101
+ final_df = pd.concat(dfs, ignore_index=True)
102
+ final_df = final_df.sort_values("ds").reset_index(drop=True)
103
+
104
+ # Aggregate to daily if not already (The helpers return daily)
105
+ # But we need to ensure unique dates in case of overlap
106
+ final_df = final_df.drop_duplicates(subset=["ds"], keep="last")
107
+
108
+ # Filter to ensure exact range (handling timezone spillover)
109
+ final_df = final_df[
110
+ (final_df["ds"].dt.date >= start_date) &
111
+ (final_df["ds"].dt.date <= end_date)
112
+ ]
113
+
114
+ return final_df.reset_index(drop=True)
115
+
116
+ def _fetch_historical_api(self, start_date, end_date):
117
+ url = "https://archive-api.open-meteo.com/v1/archive"
118
+ params = {
119
+ "latitude": self.latitude,
120
+ "longitude": self.longitude,
121
+ "start_date": start_date.strftime("%Y-%m-%d"),
122
+ "end_date": end_date.strftime("%Y-%m-%d"),
123
+ "daily": ["temperature_2m_mean", "rain_sum"], # We need these for consistency check, but we use hourly aggregated
124
+ "hourly": ["temperature_2m", "relative_humidity_2m", "rain"],
125
+ "timezone": "auto",
126
+ }
127
+
128
+ responses = self.openmeteo.weather_api(url, params=params)
129
+ response = responses[0]
130
+
131
+ hourly = response.Hourly()
132
+ hourly_data = {
133
+ "date": pd.date_range(
134
+ start=pd.to_datetime(hourly.Time(), unit="s", utc=True),
135
+ end=pd.to_datetime(hourly.TimeEnd(), unit="s", utc=True),
136
+ freq=pd.Timedelta(seconds=hourly.Interval()),
137
+ inclusive="left",
138
+ ),
139
+ "temperature_2m": hourly.Variables(0).ValuesAsNumpy(),
140
+ "relative_humidity_2m": hourly.Variables(1).ValuesAsNumpy(),
141
+ "rain": hourly.Variables(2).ValuesAsNumpy(),
142
+ }
143
+
144
+ hourly_df = pd.DataFrame(data=hourly_data)
145
+
146
+ # Daily aggregation
147
+ hourly_df["ds"] = hourly_df["date"].dt.floor("D").dt.tz_convert(None)
148
+ daily_weather = (
149
+ hourly_df.groupby("ds")
150
+ .agg(
151
+ temp=("temperature_2m", "mean"),
152
+ humidity=("relative_humidity_2m", "mean"),
153
+ rainfall=("rain", "sum"),
154
+ )
155
+ .reset_index()
156
+ )
157
+ return daily_weather
158
+
159
+ def _fetch_ensemble_forecast_api(self, forecast_days):
160
+ url = "https://ensemble-api.open-meteo.com/v1/ensemble"
161
+ params = {
162
+ "latitude": self.latitude,
163
+ "longitude": self.longitude,
164
+ "hourly": ["temperature_2m", "relative_humidity_2m", "rain"],
165
+ "models": ["ecmwf_ifs025", "gfs025", "icon_global", "icon_seamless", "gem_global", "bom_access_global_ensemble"],
166
+ "timezone": "auto",
167
+ "forecast_days": forecast_days,
168
+ }
169
+ responses = self.openmeteo.weather_api(url, params=params)
170
+
171
+ # We will aggregate all models and members into a single mean
172
+ all_hourly_dfs = []
173
+
174
+ for response in responses:
175
+ hourly = response.Hourly()
176
+
177
+ # Helper to extract all members for a variable
178
+ def get_members(variable_type):
179
+ # variable_type is an enum from Variable class?
180
+ # The snippet uses filter on hourly.Variables()
181
+ # We need to map the snippet logic here.
182
+ vars_list = [hourly.Variables(i) for i in range(hourly.VariablesLength())]
183
+ return [v for v in vars_list if v.Variable() == variable_type]
184
+
185
+ # We need Variable enum.
186
+ # Note: The snippet imports Variable.
187
+ # We need to check if Variable.temperature is correct mapping for "temperature_2m"
188
+ # In snippet: Variable.temperature and Altitude() == 2
189
+
190
+ temp_vars = [v for v in [hourly.Variables(i) for i in range(hourly.VariablesLength())]
191
+ if v.Variable() == Variable.temperature and v.Altitude() == 2]
192
+
193
+ rh_vars = [v for v in [hourly.Variables(i) for i in range(hourly.VariablesLength())]
194
+ if v.Variable() == Variable.relative_humidity and v.Altitude() == 2]
195
+
196
+ rain_vars = [v for v in [hourly.Variables(i) for i in range(hourly.VariablesLength())]
197
+ if v.Variable() == Variable.rain]
198
+
199
+ # Create a DF for this model
200
+ dates = pd.date_range(
201
+ start=pd.to_datetime(hourly.Time(), unit="s", utc=True),
202
+ end=pd.to_datetime(hourly.TimeEnd(), unit="s", utc=True),
203
+ freq=pd.Timedelta(seconds=hourly.Interval()),
204
+ inclusive="left",
205
+ )
206
+
207
+ model_df = pd.DataFrame({"date": dates})
208
+
209
+ # Average members for this model
210
+ if temp_vars:
211
+ temps = np.stack([v.ValuesAsNumpy() for v in temp_vars])
212
+ model_df["temp"] = np.mean(temps, axis=0)
213
+ else:
214
+ model_df["temp"] = np.nan
215
+
216
+ if rh_vars:
217
+ rhs = np.stack([v.ValuesAsNumpy() for v in rh_vars])
218
+ model_df["humidity"] = np.mean(rhs, axis=0)
219
+ else:
220
+ model_df["humidity"] = np.nan
221
+
222
+ if rain_vars:
223
+ rains = np.stack([v.ValuesAsNumpy() for v in rain_vars])
224
+ model_df["rainfall"] = np.mean(rains, axis=0)
225
+ else:
226
+ model_df["rainfall"] = 0
227
+
228
+ all_hourly_dfs.append(model_df)
229
+
230
+ # Concatenate all models
231
+ full_hourly = pd.concat(all_hourly_dfs, ignore_index=True)
232
+
233
+ # Group by date and take mean across all models
234
+ full_hourly = full_hourly.groupby("date").mean().reset_index()
235
+
236
+ # Daily aggregation
237
+ full_hourly["ds"] = full_hourly["date"].dt.floor("D").dt.tz_convert(None)
238
+ daily_weather = (
239
+ full_hourly.groupby("ds")
240
+ .agg(
241
+ temp=("temp", "mean"),
242
+ humidity=("humidity", "mean"),
243
+ rainfall=("rainfall", "sum"),
244
+ )
245
+ .reset_index()
246
+ )
247
+
248
+ return daily_weather
249
+
250
+ def preprocess_data(self, df):
251
+ """
252
+ Preprocesses the input dataframe:
253
+ 1. Fetches historical weather
254
+ 2. Creates lag/diff features
255
+ 3. Scales data
256
+ """
257
+ df = df.copy()
258
+ df["ds"] = pd.to_datetime(df["ds"])
259
+ df = df.sort_values("ds").reset_index(drop=True)
260
+ df["unique_id"] = "VV"
261
+
262
+ # 1. Fetch Historical Weather
263
+ start_date = df["ds"].min().date()
264
+ end_date = df["ds"].max().date()
265
+ print(f"Fetching historical weather from {start_date} to {end_date}...")
266
+ weather_df = self.fetch_weather_data(start_date, end_date)
267
+
268
+ df = df.merge(weather_df, on="ds", how="left")
269
+ df[self.futr_exog_list] = df[self.futr_exog_list].ffill().bfill()
270
+
271
+ # 2. Feature Engineering (Lags/Diffs)
272
+ df["lag1"] = df["y"].shift(1)
273
+ df["diff"] = df["y"].diff()
274
+
275
+ df = df.dropna().reset_index(drop=True)
276
+
277
+ # 3. Scaling
278
+ df["y"] = self.y_scaler.fit_transform(df[["y"]])
279
+
280
+ all_exog = self.hist_exog_list + self.futr_exog_list
281
+ df[all_exog] = self.exog_scaler.fit_transform(df[all_exog])
282
+
283
+ return df
284
+
285
+ def tune_and_predict(self, csv_path, field_coords, target_col="y", output_file="auto_tuned_predictions.csv", num_samples=10):
286
+ """
287
+ Runs AutoNHITS tuning and predicts the next 20 days.
288
+ """
289
+ print(f"Setting coordinates to: {field_coords}")
290
+ self.set_coordinates(field_coords)
291
+
292
+ # 1. Load Data
293
+ print("Loading data...")
294
+ df = pd.read_csv(csv_path)
295
+
296
+ # Rename target column to 'y' if it exists
297
+ if target_col in df.columns:
298
+ df = df.rename(columns={target_col: "y"})
299
+
300
+ if "ds" not in df.columns or "y" not in df.columns:
301
+ raise ValueError(f"CSV must contain 'ds' and '{target_col}' (mapped to 'y') columns.")
302
+
303
+ # 2. Preprocess
304
+ print("Preprocessing data...")
305
+ train_df = self.preprocess_data(df)
306
+
307
+ # 3. Prepare Future Dataframe
308
+ last_date = train_df["ds"].max()
309
+ # Ensure we predict for at least 30 days as per user request "next 1 month"
310
+ # Data frequency is 5 Days, so 30 days / 5 = 6 periods
311
+ prediction_days = 6
312
+ # Start 5 days after the last training date
313
+ future_dates = pd.date_range(start=last_date + pd.Timedelta(days=5), periods=prediction_days, freq="5D")
314
+ future_df = pd.DataFrame({"ds": future_dates, "unique_id": "VV"})
315
+
316
+ # 4. Fetch Future Weather
317
+ print("Fetching future weather forecast...")
318
+ # The new fetch_weather_data handles the logic automatically
319
+ weather_future = self.fetch_weather_data(future_dates[0].date(), future_dates[-1].date())
320
+
321
+ future_df = future_df.merge(weather_future, on="ds", how="left")
322
+ future_df[self.futr_exog_list] = future_df[self.futr_exog_list].fillna(0)
323
+
324
+ # 5. Auto Model Definition
325
+ import ray.tune as tune
326
+ print(f"Initializing AutoNHITS model (tuning with {num_samples} samples)...")
327
+
328
+ # Define a custom search space (same as auto_tuning_testing.py)
329
+ config = {
330
+ "input_size": tune.choice([60, 90, 120]), # Lookback window
331
+ "learning_rate": tune.loguniform(1e-4, 1e-2), # Learning rate
332
+ "n_blocks": tune.choice([[1, 1, 1], [3, 3, 3]]), # Depth
333
+ "mlp_units": tune.choice([ # Width
334
+ [[64, 64], [64, 64], [64, 64]],
335
+ [[512, 512], [512, 512], [512, 512]]
336
+ ]),
337
+ "n_pool_kernel_size": tune.choice([ # Pooling
338
+ [2, 2, 1],
339
+ [4, 4, 2],
340
+ [8, 4, 1]
341
+ ]),
342
+ "n_freq_downsample": tune.choice([ # Downsampling
343
+ [2, 1, 1],
344
+ [4, 2, 1],
345
+ [8, 4, 1]
346
+ ])
347
+ }
348
+
349
+ # AutoNHITS configuration
350
+ auto_nhits = AutoNHITS(
351
+ h=prediction_days, # Horizon (6 steps = 30 days)
352
+ loss=MAE(),
353
+ config=config,
354
+ search_alg=None, # Use default search algorithm (HyperOpt)
355
+ num_samples=num_samples, # Number of trials
356
+ cpus=1,
357
+ gpus=0, # Set to 1 if GPU available
358
+ verbose=True,
359
+ alias="AutoNHITS"
360
+ )
361
+
362
+ nf = NeuralForecast(models=[auto_nhits], freq="5D")
363
+
364
+ # 6. Train (Tune) and Predict
365
+ print("Tuning and Training model...")
366
+ nf.fit(df=train_df)
367
+
368
+ # Get best config
369
+ # The model inside nf.models[0] is the trained AutoNHITS
370
+ # It should have 'results' or 'best_config' attribute after fitting?
371
+ # Actually, AutoNHITS replaces itself with the best model found or wraps it.
372
+
373
+ print("Predicting...")
374
+ # Prepare future exogenous features
375
+ # Note: We need to scale them!
376
+ X_futr = future_df[self.futr_exog_list].values
377
+
378
+ # Get indices of futr_exog in the scaler
379
+ # self.exog_scaler was fitted on [hist_exog + futr_exog]
380
+ # futr_exog are the last columns, starting after hist_exog
381
+ start_idx = len(self.hist_exog_list)
382
+ futr_indices = [start_idx + i for i in range(len(self.futr_exog_list))]
383
+
384
+ means = self.exog_scaler.mean_[futr_indices]
385
+ scales = self.exog_scaler.scale_[futr_indices]
386
+
387
+ X_futr_scaled = (X_futr - means) / scales
388
+ future_df[self.futr_exog_list] = X_futr_scaled
389
+
390
+ preds_df = nf.predict(futr_df=future_df)
391
+
392
+ # 7. Inverse Scale Predictions
393
+ y_pred_scaled = preds_df["AutoNHITS"].values.reshape(-1, 1)
394
+ y_pred = self.y_scaler.inverse_transform(y_pred_scaled).flatten()
395
+
396
+ result = pd.DataFrame({
397
+ "ds": preds_df["ds"].values,
398
+ "predicted_y": y_pred
399
+ })
400
+
401
+ # 8. Plot results
402
+ result.to_csv(output_file, index=False)
403
+ print(f"\nPredictions saved to {output_file}")
404
+
405
+ return result
406
+
407
+ try:
408
+ from plot_predictions import plot_predictions
409
+ print("\nGenerating plot...")
410
+ plot_predictions(csv_path, output_file)
411
+ except Exception as e:
412
+ print(f"Warning: Could not generate plot: {e}")
413
+
414
+ return result
415
+
416
+ if __name__ == "__main__":
417
+ # Define the input file path here
418
+ target_file = "vh_data_structured.csv"
419
+
420
+ print(f"Using input file: {target_file}")
421
+
422
+ if os.path.exists(target_file):
423
+ predictor = AutoTimeSeriesPredictor()
424
+
425
+ # Example coordinates
426
+ coords = [
427
+ (77.2090, 28.6139),
428
+ (77.2100, 28.6139),
429
+ (77.2100, 28.6149),
430
+ (77.2090, 28.6149),
431
+ (77.2090, 28.6139),
432
+ ]
433
+
434
+ try:
435
+ predictions = predictor.tune_and_predict(target_file, field_coords=coords, num_samples=30) # 30 samples for better results
436
+ print("\nPredictions for the next 20 days:")
437
+ print(predictions)
438
+ except Exception as e:
439
+ print(f"An error occurred: {e}")
440
+ else:
441
+ print(f"File not found: {target_file}")
prediction_pipeline.py ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Prediction Pipeline
3
+ Orchestrates satellite data fetching and time series prediction.
4
+ """
5
+
6
+ import os
7
+ import pandas as pd
8
+ from satellite_pipeline import SatelliteFetcher
9
+ from auto_tuning_predictor import AutoTimeSeriesPredictor
10
+
11
+ class PredictionOrchestrator:
12
+ """
13
+ Orchestrates the entire pipeline:
14
+ 1. Fetch Satellite Data (SAR & Sentinel-2)
15
+ 2. Generate Predictions for ALL bands using AutoNHITS
16
+ """
17
+
18
+ def __init__(self, polygon_coords):
19
+ """
20
+ Initialize the orchestrator.
21
+
22
+ Args:
23
+ polygon_coords (list): List of (lon, lat) tuples defining the polygon.
24
+ """
25
+ self.polygon_coords = polygon_coords
26
+ self.sar_csv = 'sar_data.csv'
27
+ self.sentinel2_csv = 'sentinel2_data.csv'
28
+
29
+ self.sar_pred_csv = 'sar_predictions.csv'
30
+ self.sentinel2_pred_csv = 'sentinel2_predictions.csv'
31
+
32
+ def run(self):
33
+ """Run the full pipeline."""
34
+ print("="*60)
35
+ print("STARTING PREDICTION PIPELINE")
36
+ print("="*60)
37
+
38
+ # 1. Fetch Satellite Data
39
+ print("\n[1/2] Fetching Satellite Data...")
40
+ fetcher = SatelliteFetcher(self.polygon_coords)
41
+ fetcher.run_all()
42
+ # print("Skipping fetch for verification (using existing CSVs)...")
43
+
44
+ # 2. Generate Predictions
45
+ print("\n[2/2] Generating Predictions...")
46
+ predictor = AutoTimeSeriesPredictor()
47
+
48
+ # --- SAR Prediction ---
49
+ print("\n" + "-"*30)
50
+ print("Predicting SAR Data (All Bands)")
51
+ print("-"*30)
52
+ self._predict_all_bands(
53
+ predictor,
54
+ input_csv=self.sar_csv,
55
+ output_csv=self.sar_pred_csv
56
+ )
57
+
58
+ # --- Sentinel-2 Prediction ---
59
+ print("\n" + "-"*30)
60
+ print("Predicting Sentinel-2 Data (All Bands)")
61
+ print("-"*30)
62
+ self._predict_all_bands(
63
+ predictor,
64
+ input_csv=self.sentinel2_csv,
65
+ output_csv=self.sentinel2_pred_csv
66
+ )
67
+
68
+ print("\n" + "="*60)
69
+ print("PIPELINE COMPLETE")
70
+ print(f"SAR Predictions: {self.sar_pred_csv}")
71
+ print(f"Sentinel-2 Predictions: {self.sentinel2_pred_csv}")
72
+ print("="*60)
73
+
74
+ def _predict_all_bands(self, predictor, input_csv, output_csv):
75
+ """
76
+ Helper to predict all numerical columns in a CSV.
77
+ """
78
+ if not os.path.exists(input_csv):
79
+ print(f"❌ Input file not found: {input_csv}")
80
+ return
81
+
82
+ df = pd.read_csv(input_csv)
83
+ if 'ds' not in df.columns:
84
+ print(f"❌ 'ds' column missing in {input_csv}")
85
+ return
86
+
87
+ # Identify target columns (all except 'ds')
88
+ target_cols = [c for c in df.columns if c != 'ds']
89
+ print(f"Found {len(target_cols)} targets: {target_cols}")
90
+
91
+ all_preds = []
92
+
93
+ for col in target_cols:
94
+ print(f"\n>> Predicting {col}...")
95
+ try:
96
+ # We use a temporary output file for individual band predictions
97
+ temp_out = f"temp_pred_{col}.csv"
98
+
99
+ pred_df = predictor.tune_and_predict(
100
+ csv_path=input_csv,
101
+ field_coords=self.polygon_coords,
102
+ target_col=col,
103
+ output_file=temp_out,
104
+ num_samples=5
105
+ )
106
+
107
+ # Rename predicted column to the band name
108
+ pred_df = pred_df.rename(columns={'predicted_y': col})
109
+
110
+ if not all_preds:
111
+ all_preds.append(pred_df)
112
+ else:
113
+ # Merge on 'ds'
114
+ all_preds.append(pred_df[['ds', col]])
115
+
116
+ # Clean up temp file
117
+ if os.path.exists(temp_out):
118
+ os.remove(temp_out)
119
+
120
+ except Exception as e:
121
+ print(f"❌ Failed to predict {col}: {e}")
122
+
123
+ # Merge all predictions
124
+ if all_preds:
125
+ final_df = all_preds[0]
126
+ for i in range(1, len(all_preds)):
127
+ final_df = final_df.merge(all_preds[i], on='ds', how='outer')
128
+
129
+ final_df.to_csv(output_csv, index=False)
130
+ print(f"βœ“ Saved combined predictions to {output_csv}")
131
+ else:
132
+ print("⚠ No predictions generated.")
133
+
134
+ if __name__ == "__main__":
135
+ # Example Usage
136
+ POLYGON = [
137
+ (75.829, 30.229),
138
+ (75.831, 30.229),
139
+ (75.831, 30.231),
140
+ (75.829, 30.231),
141
+ (75.829, 30.229)
142
+ ]
143
+
144
+ pipeline = PredictionOrchestrator(POLYGON)
145
+ pipeline.run()
requirements.txt ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.104.1
2
+ uvicorn[standard]==0.24.0
3
+ pydantic==2.5.2
4
+ numpy>=1.24.0
5
+ pandas>=2.0.0
6
+ shapely>=2.0.0
7
+ python-dotenv>=1.0.0
8
+ sentinelhub>=3.9.0
9
+ scikit-learn>=1.3.0
10
+ matplotlib>=3.7.0
11
+ neuralforecast>=1.6.0
12
+ openmeteo-requests>=1.2.0
13
+ requests-cache>=1.1.0
14
+ retry-requests>=2.0.0
15
+ openmeteo-sdk>=1.7.0
16
+ ray[tune]>=2.7.0
17
+ torch>=2.0.0
satellite_pipeline.py ADDED
@@ -0,0 +1,311 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Satellite Data Pipeline
3
+ Fetches both Sentinel-1 (SAR) and Sentinel-2 (Optical) data for a given polygon.
4
+ Outputs two separate CSV files: `sar_data.csv` and `sentinel2_data.csv`.
5
+ """
6
+
7
+ import os
8
+ import datetime
9
+ import numpy as np
10
+ import pandas as pd
11
+ from shapely.geometry import Polygon
12
+ from dotenv import load_dotenv
13
+
14
+ from sentinelhub import (
15
+ SHConfig,
16
+ SentinelHubRequest,
17
+ SentinelHubCatalog,
18
+ DataCollection,
19
+ MimeType,
20
+ BBox,
21
+ CRS,
22
+ bbox_to_dimensions,
23
+ Geometry
24
+ )
25
+
26
+ # Load environment variables
27
+ load_dotenv()
28
+
29
+ class SatelliteFetcher:
30
+ """
31
+ A class to fetch and process satellite data (SAR and Optical) for a specific area of interest.
32
+ """
33
+
34
+ def __init__(self, polygon_coords):
35
+ """
36
+ Initialize the fetcher.
37
+
38
+ Args:
39
+ polygon_coords (list): List of (lon, lat) tuples defining the polygon.
40
+ """
41
+ self.polygon_coords = polygon_coords
42
+ self.start_date = '2020-01-01'
43
+ self.end_date = datetime.date.today().strftime('%Y-%m-%d')
44
+
45
+ # Configuration
46
+ self.resolution = 10
47
+ self.max_cloud_cover = 20.0
48
+
49
+ # Setup Sentinel Hub
50
+ self.config = self._setup_config()
51
+ self.geometry, self.bbox, self.size = self._setup_geometry()
52
+
53
+ def _setup_config(self):
54
+ """Configure Sentinel Hub credentials."""
55
+ config = SHConfig()
56
+ config.sh_client_id = os.environ.get('SH_CLIENT_ID')
57
+ config.sh_client_secret = os.environ.get('SH_CLIENT_SECRET')
58
+
59
+ if not config.sh_client_id:
60
+ config.sh_client_id = "sh-4c23abb9-6263-4a2c-bdba-a4ff6b84bdfb"
61
+ if not config.sh_client_secret:
62
+ config.sh_client_secret = "iZ4gexrfiWQpopHAGYlBeEMj9J8DAZtD"
63
+
64
+ config.sh_base_url = 'https://sh.dataspace.copernicus.eu'
65
+ config.sh_token_url = 'https://identity.dataspace.copernicus.eu/auth/realms/CDSE/protocol/openid-connect/token'
66
+ config.save("cdse")
67
+
68
+ return config
69
+
70
+ def _setup_geometry(self):
71
+ """Setup geometry, bbox, and size from polygon coordinates."""
72
+ shapely_poly = Polygon(self.polygon_coords)
73
+ geometry = Geometry(shapely_poly, crs=CRS.WGS84)
74
+ bbox = geometry.bbox
75
+ size = bbox_to_dimensions(bbox, resolution=self.resolution)
76
+
77
+ print(f"AOI Setup:")
78
+ print(f" Polygon: {self.polygon_coords}")
79
+ print(f" BBox: {bbox}")
80
+ print(f" Size: {size}")
81
+ print(f" Time Range: {self.start_date} to {self.end_date}")
82
+
83
+ return geometry, bbox, size
84
+
85
+ def fetch_sar_data(self, output_csv='sar_data.csv'):
86
+ """
87
+ Fetch Sentinel-1 SAR data (VV, VH) and save to CSV.
88
+ """
89
+ print("\n" + "="*40)
90
+ print("FETCHING SENTINEL-1 SAR DATA")
91
+ print("="*40)
92
+
93
+ S1 = DataCollection.define(
94
+ name="SENTINEL1_IW_CDSE",
95
+ api_id="sentinel-1-grd",
96
+ service_url="https://sh.dataspace.copernicus.eu"
97
+ )
98
+
99
+ evalscript = """
100
+ //VERSION=3
101
+ function setup() {
102
+ return {
103
+ input: ["VV", "VH", "dataMask"],
104
+ output: [
105
+ { id: "VV", bands: 1, sampleType: "FLOAT32" },
106
+ { id: "VH", bands: 1, sampleType: "FLOAT32" }
107
+ ]
108
+ };
109
+ }
110
+
111
+ function evaluatePixel(sample) {
112
+ let vv_db = (sample.VV > 0) ? 10 * Math.log(sample.VV) / Math.LN10 : -9999;
113
+ let vh_db = (sample.VH > 0) ? 10 * Math.log(sample.VH) / Math.LN10 : -9999;
114
+ return { VV: [vv_db], VH: [vh_db] };
115
+ }
116
+ """
117
+
118
+ print("πŸ” Searching catalog...")
119
+ catalog = SentinelHubCatalog(config=self.config)
120
+ results = catalog.search(
121
+ collection=S1,
122
+ geometry=self.geometry,
123
+ time=(self.start_date, self.end_date),
124
+ filter="sar:instrument_mode = 'IW'"
125
+ )
126
+
127
+ scenes = list(results)
128
+ dates = sorted(list(set([scene['properties']['datetime'].split('T')[0] for scene in scenes])))
129
+ print(f"βœ“ Found {len(dates)} unique dates.")
130
+
131
+ all_records = []
132
+
133
+ print("πŸ“₯ Downloading and Processing...")
134
+ for date_str in dates:
135
+ dt = datetime.datetime.strptime(date_str, "%Y-%m-%d")
136
+ next_day = (dt + datetime.timedelta(days=1)).strftime("%Y-%m-%d")
137
+
138
+ request = SentinelHubRequest(
139
+ evalscript=evalscript,
140
+ input_data=[
141
+ SentinelHubRequest.input_data(
142
+ data_collection=S1,
143
+ time_interval=(date_str, next_day),
144
+ mosaicking_order="leastRecent",
145
+ other_args={"processing": {"backCoeff": "GAMMA0_TERRAIN", "orthorectify": True}}
146
+ )
147
+ ],
148
+ responses=[
149
+ SentinelHubRequest.output_response('VV', MimeType.TIFF),
150
+ SentinelHubRequest.output_response('VH', MimeType.TIFF)
151
+ ],
152
+ geometry=self.geometry,
153
+ bbox=self.bbox,
154
+ size=self.size,
155
+ config=self.config
156
+ )
157
+
158
+ try:
159
+ data = request.get_data()
160
+ if data and len(data) > 0:
161
+ data_dict = data[0]
162
+ if 'VV.tif' in data_dict and 'VH.tif' in data_dict:
163
+ vv_arr = data_dict['VV.tif']
164
+ vh_arr = data_dict['VH.tif']
165
+
166
+ valid_mask = (vv_arr > -9999) & (vh_arr > -9999)
167
+
168
+ if np.any(valid_mask):
169
+ vv_mean = np.mean(vv_arr[valid_mask])
170
+ vh_mean = np.mean(vh_arr[valid_mask])
171
+
172
+ all_records.append({
173
+ 'ds': date_str,
174
+ 'VV_mean_dB': round(vv_mean, 4),
175
+ 'VH_mean_dB': round(vh_mean, 4)
176
+ })
177
+ print(f" βœ“ {date_str}: VV={vv_mean:.2f}, VH={vh_mean:.2f}")
178
+ else:
179
+ print(f" ⚠ {date_str}: No valid pixels")
180
+ except Exception as e:
181
+ print(f" ❌ Error {date_str}: {e}")
182
+
183
+ df = pd.DataFrame(all_records)
184
+ if not df.empty:
185
+ df = df.sort_values('ds').reset_index(drop=True)
186
+ df.to_csv(output_csv, index=False)
187
+ print(f"\nπŸ’Ύ Saved SAR data to: {output_csv}")
188
+ else:
189
+ print("\n⚠ No SAR data fetched.")
190
+
191
+ def fetch_sentinel2_data(self, output_csv='sentinel2_data.csv'):
192
+ """
193
+ Fetch Sentinel-2 L2A data (Bands + SCL) and save to CSV.
194
+ """
195
+ print("\n" + "="*40)
196
+ print("FETCHING SENTINEL-2 OPTICAL DATA")
197
+ print("="*40)
198
+
199
+ S2 = DataCollection.define(
200
+ name="SENTINEL2_L2A_CDSE",
201
+ api_id="sentinel-2-l2a",
202
+ service_url="https://sh.dataspace.copernicus.eu",
203
+ collection_type="Sentinel-2",
204
+ is_timeless=False
205
+ )
206
+
207
+ evalscript = """
208
+ //VERSION=3
209
+ function setup() {
210
+ return {
211
+ input: [{
212
+ bands: ["B01", "B02", "B03", "B04", "B05", "B06", "B07", "B08", "B8A", "B09", "B11", "B12", "SCL", "dataMask"],
213
+ units: "DN"
214
+ }],
215
+ output: {
216
+ bands: 13,
217
+ sampleType: "FLOAT32"
218
+ }
219
+ };
220
+ }
221
+
222
+ function evaluatePixel(sample) {
223
+ if (sample.dataMask == 0) {
224
+ return [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
225
+ }
226
+ return [
227
+ sample.B01 / 10000, sample.B02 / 10000, sample.B03 / 10000, sample.B04 / 10000,
228
+ sample.B05 / 10000, sample.B06 / 10000, sample.B07 / 10000, sample.B08 / 10000,
229
+ sample.B8A / 10000, sample.B09 / 10000, sample.B11 / 10000, sample.B12 / 10000,
230
+ sample.SCL
231
+ ];
232
+ }
233
+ """
234
+
235
+ print("πŸ” Searching catalog...")
236
+ catalog = SentinelHubCatalog(config=self.config)
237
+ search_iterator = catalog.search(
238
+ S2,
239
+ geometry=self.geometry,
240
+ time=(self.start_date, self.end_date),
241
+ filter=f'eo:cloud_cover < {self.max_cloud_cover}'
242
+ )
243
+
244
+ dates = sorted(list(set([item['properties']['datetime'] for item in search_iterator])))
245
+ print(f"βœ“ Found {len(dates)} available scenes.")
246
+
247
+ results = []
248
+ band_names = ["B01", "B02", "B03", "B04", "B05", "B06", "B07", "B08", "B8A", "B09", "B11", "B12"]
249
+
250
+ print("πŸ“₯ Downloading and Processing...")
251
+ for i, date_str in enumerate(dates):
252
+ request = SentinelHubRequest(
253
+ evalscript=evalscript,
254
+ input_data=[SentinelHubRequest.input_data(
255
+ data_collection=S2,
256
+ time_interval=(date_str, date_str)
257
+ )],
258
+ responses=[SentinelHubRequest.output_response('default', MimeType.TIFF)],
259
+ geometry=self.geometry,
260
+ bbox=self.bbox,
261
+ size=self.size,
262
+ config=self.config
263
+ )
264
+
265
+ try:
266
+ data = request.get_data()[0]
267
+ scl = data[:, :, 12]
268
+ valid_mask = (scl != 0)
269
+
270
+ if np.any(valid_mask):
271
+ means = {}
272
+ for b_idx, b_name in enumerate(band_names):
273
+ means[b_name] = np.mean(data[:, :, b_idx][valid_mask])
274
+
275
+ means['ds'] = date_str.split('T')[0]
276
+ results.append(means)
277
+ print(f" βœ“ {date_str[:10]}")
278
+ else:
279
+ print(f" ⚠ {date_str[:10]} (No valid pixels)")
280
+ except Exception as e:
281
+ print(f" ❌ Error {date_str[:10]}: {e}")
282
+
283
+ df = pd.DataFrame(results)
284
+ if not df.empty:
285
+ cols = ['ds'] + [c for c in df.columns if c != 'ds']
286
+ df = df[cols]
287
+ df.to_csv(output_csv, index=False)
288
+ print(f"\nπŸ’Ύ Saved Sentinel-2 data to: {output_csv}")
289
+ else:
290
+ print("\n⚠ No Sentinel-2 data fetched.")
291
+
292
+ def run_all(self):
293
+ """Run both pipelines."""
294
+ self.fetch_sar_data()
295
+ self.fetch_sentinel2_data()
296
+
297
+
298
+ if __name__ == "__main__":
299
+ # Example Usage
300
+ # PAU Experimental Farm, Punjab, India
301
+ POLYGON = [
302
+ (75.829, 30.229),
303
+ (75.831, 30.229),
304
+ (75.831, 30.231),
305
+ (75.829, 30.231),
306
+ (75.829, 30.229)
307
+ ]
308
+
309
+ # Initialize and Run
310
+ fetcher = SatelliteFetcher(POLYGON)
311
+ fetcher.run_all()