Aniket2006 commited on
Commit
2d332ab
·
1 Parent(s): 8ad7851

Deploy Sentinel-2 Pipeline

Browse files
DEPLOYMENT_CHECKLIST.md ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Production Pipeline - Files to Send to Developer
2
+
3
+ ## Core Pipeline Files (Required)
4
+
5
+ 1. **crop_stress_pipeline.py** - Main pipeline orchestrator
6
+ 2. **vegetation_indices.py** - Vegetation indices calculation module
7
+ 3. **stress_detection_preprocessing.py** - Data preprocessing for deep learning
8
+ 4. **stress_detection_model.py** - CNN+LSTM stress detection model
9
+ 5. **llm_analysis.py** - LLM integration and prompt engineering
10
+
11
+ ## Configuration Files (Required)
12
+
13
+ 6. **requirements.txt** - Python dependencies
14
+ 7. **.env.template** - Environment variables template (rename to .env and fill in credentials)
15
+
16
+ ## Documentation (Required)
17
+
18
+ 8. **DEVELOPER_GUIDE.md** - Complete developer documentation
19
+ 9. **DEPLOYMENT_CHECKLIST.md** - This file
20
+
21
+ ---
22
+
23
+ ## Deployment Checklist
24
+
25
+ ### Pre-Deployment
26
+
27
+ - [ ] Install Python 3.9+ on target server
28
+ - [ ] Create virtual environment: `python -m venv venv`
29
+ - [ ] Activate virtual environment
30
+ - [ ] Install dependencies: `pip install -r requirements.txt`
31
+ - [ ] Copy `.env.template` to `.env`
32
+ - [ ] Fill in Sentinel Hub credentials in `.env`
33
+ - [ ] Fill in Gemini API key in `.env`
34
+ - [ ] Test Sentinel Hub connection
35
+ - [ ] Test Gemini API connection
36
+
37
+ ### Testing
38
+
39
+ - [ ] Run test with sample coordinates
40
+ - [ ] Verify log file is created
41
+ - [ ] Verify JSON output is generated
42
+ - [ ] Check all 13 vegetation indices are calculated
43
+ - [ ] Verify stress detection runs successfully
44
+ - [ ] Confirm LLM analysis completes
45
+ - [ ] Review output JSON structure
46
+
47
+ ### Production Deployment
48
+
49
+ - [ ] Set up logging directory with write permissions
50
+ - [ ] Configure log rotation (optional)
51
+ - [ ] Set up monitoring for pipeline failures
52
+ - [ ] Configure API rate limits (Gemini: 60 requests/min)
53
+ - [ ] Set up backup for output JSON files
54
+ - [ ] Document server specifications (min 4GB RAM)
55
+ - [ ] Create systemd service (Linux) or Windows Service (optional)
56
+
57
+ ### Security
58
+
59
+ - [ ] Ensure `.env` file is NOT committed to version control
60
+ - [ ] Add `.env` to `.gitignore`
61
+ - [ ] Restrict file permissions on `.env` (chmod 600)
62
+ - [ ] Use environment-specific credentials (dev/staging/prod)
63
+ - [ ] Rotate API keys regularly
64
+ - [ ] Enable HTTPS for API endpoints (if exposing via REST)
65
+
66
+ ### Monitoring
67
+
68
+ - [ ] Set up log monitoring (e.g., ELK stack, CloudWatch)
69
+ - [ ] Configure alerts for pipeline failures
70
+ - [ ] Monitor API quota usage (Sentinel Hub, Gemini)
71
+ - [ ] Track processing time metrics
72
+ - [ ] Monitor disk space for output files
73
+
74
+ ---
75
+
76
+ ## Quick Test Script
77
+
78
+ ```python
79
+ # test_pipeline.py
80
+ from crop_stress_pipeline import CropStressPipeline
81
+
82
+ pipeline = CropStressPipeline()
83
+
84
+ # Test with PAU Experimental Farm
85
+ params = {
86
+ 'center_lat': 30.2300,
87
+ 'center_lon': 75.8300,
88
+ 'crop_type': 'Wheat',
89
+ 'analysis_date': '2024-01-15',
90
+ 'field_size_hectares': 0.04,
91
+ 'farmer_context': {
92
+ 'role': 'Owner-Operator',
93
+ 'years_farming': 15,
94
+ 'irrigation_method': 'Drip Irrigation',
95
+ 'farming_goal': 'Maximize yield'
96
+ },
97
+ 'output_path': 'test_results.json'
98
+ }
99
+
100
+ try:
101
+ results = pipeline.run(**params)
102
+ print("✓ Pipeline test successful!")
103
+ print(f"✓ Output saved to: {params['output_path']}")
104
+ except Exception as e:
105
+ print(f"✗ Pipeline test failed: {e}")
106
+ ```
107
+
108
+ ---
109
+
110
+ ## Expected Output Structure
111
+
112
+ ```
113
+ production_pipeline/
114
+ ├── crop_stress_pipeline.py
115
+ ├── vegetation_indices.py
116
+ ├── stress_detection_preprocessing.py
117
+ ├── stress_detection_model.py
118
+ ├── llm_analysis.py
119
+ ├── requirements.txt
120
+ ├── .env.template
121
+ ├── .env (create from template)
122
+ ├── DEVELOPER_GUIDE.md
123
+ ├── DEPLOYMENT_CHECKLIST.md
124
+ ├── crop_stress_pipeline.log (auto-generated)
125
+ └── *.json (output files)
126
+ ```
127
+
128
+ ---
129
+
130
+ ## API Quotas & Limits
131
+
132
+ ### Sentinel Hub (CDSE)
133
+ - **Free tier**: 30,000 processing units/month
134
+ - **Rate limit**: ~10 requests/second
135
+ - **Typical usage**: ~100 PU per field analysis
136
+
137
+ ### Google Gemini
138
+ - **Free tier**: 60 requests/minute
139
+ - **Rate limit**: 1500 requests/day (free)
140
+ - **Typical usage**: 1 request per field analysis
141
+
142
+ ---
143
+
144
+ ## Troubleshooting Common Issues
145
+
146
+ ### Issue: "Sentinel Hub credentials not found"
147
+ **Solution**: Ensure `.env` file exists and contains valid credentials
148
+
149
+ ### Issue: "No images found for date range"
150
+ **Solution**: Adjust `analysis_date` or check cloud cover threshold
151
+
152
+ ### Issue: "Gemini API quota exceeded"
153
+ **Solution**: Wait for quota reset or upgrade to paid tier
154
+
155
+ ### Issue: "Out of memory error"
156
+ **Solution**: Reduce AOI size or increase server RAM
157
+
158
+ ---
159
+
160
+ ## Support Contacts
161
+
162
+ - **Technical Issues**: Check logs in `crop_stress_pipeline.log`
163
+ - **API Issues**: Refer to DEVELOPER_GUIDE.md
164
+ - **Architecture Questions**: Review pipeline source code comments
165
+
166
+ ---
167
+
168
+ ## Version Information
169
+
170
+ - **Pipeline Version**: 1.0
171
+ - **Python Version**: 3.9+
172
+ - **TensorFlow Version**: 2.13+
173
+ - **Last Updated**: 2024-12-04
DEVELOPER_GUIDE.md ADDED
@@ -0,0 +1,386 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Crop Stress Detection Pipeline - Developer Guide
2
+
3
+ ## Overview
4
+
5
+ This production-ready pipeline performs comprehensive crop stress analysis using Sentinel-2 satellite imagery, deep learning models, and LLM-powered insights.
6
+
7
+ **Pipeline Architecture:**
8
+ 1. **Data Acquisition**: Fetch Sentinel-2 L2A imagery from Copernicus Data Space Ecosystem
9
+ 2. **Vegetation Indices**: Calculate 13 indices (NDVI, EVI, NDWI, NDRE, RECI, SMI, NDSI, PRI, PSRI, MCARI, SASI, SOMI, SFI)
10
+ 3. **Temporal Analysis**: Extract temporal statistics and trends
11
+ 4. **Stress Detection**: CNN + LSTM spatial-temporal encoding with K-Means clustering (k=3)
12
+ 5. **Anomaly Detection**: Isolation Forest for unusual patterns
13
+ 6. **LLM Analysis**: Gemini-powered comprehensive crop and soil insights
14
+
15
+ ---
16
+
17
+ ## Quick Start
18
+
19
+ ### 1. Installation
20
+
21
+ ```bash
22
+ # Create virtual environment
23
+ python -m venv venv
24
+ source venv/bin/activate # On Windows: venv\Scripts\activate
25
+
26
+ # Install dependencies
27
+ pip install -r requirements.txt
28
+ ```
29
+
30
+ ### 2. Environment Setup
31
+
32
+ Create a `.env` file in the project root:
33
+
34
+ ```env
35
+ # Sentinel Hub Credentials (Copernicus Data Space Ecosystem)
36
+ SH_CLIENT_ID=your_client_id_here
37
+ SH_CLIENT_SECRET=your_client_secret_here
38
+
39
+ # Google Gemini API Key
40
+ GEMINI_API_KEY=your_gemini_api_key_here
41
+ ```
42
+
43
+ **How to get credentials:**
44
+ - **Sentinel Hub**: Register at https://dataspace.copernicus.eu/
45
+ - **Gemini API**: Get key from https://makersuite.google.com/app/apikey
46
+
47
+ ### 3. Run Pipeline
48
+
49
+ ```python
50
+ from crop_stress_pipeline import CropStressPipeline
51
+
52
+ # Initialize pipeline
53
+ pipeline = CropStressPipeline()
54
+
55
+ # Define parameters
56
+ params = {
57
+ 'center_lat': 30.2300,
58
+ 'center_lon': 75.8300,
59
+ 'crop_type': 'Wheat',
60
+ 'analysis_date': '2024-01-15',
61
+ 'field_size_hectares': 0.04,
62
+ 'farmer_context': {
63
+ 'role': 'Owner-Operator',
64
+ 'years_farming': 15,
65
+ 'irrigation_method': 'Drip Irrigation',
66
+ 'farming_goal': 'Maximize yield while maintaining soil health'
67
+ },
68
+ 'output_path': 'results.json'
69
+ }
70
+
71
+ # Run analysis
72
+ results = pipeline.run(**params)
73
+ ```
74
+
75
+ ---
76
+
77
+ ## File Structure
78
+
79
+ ```
80
+ production_pipeline/
81
+ ├── crop_stress_pipeline.py # Main pipeline script
82
+ ├── vegetation_indices.py # Vegetation indices calculation
83
+ ├── stress_detection_preprocessing.py # Data preprocessing for DL model
84
+ ├── stress_detection_model.py # CNN+LSTM stress detection model
85
+ ├── llm_analysis.py # LLM integration and prompt engineering
86
+ ├── requirements.txt # Python dependencies
87
+ ├── .env # Environment variables (create this)
88
+ ├── DEVELOPER_GUIDE.md # This file
89
+ └── crop_stress_pipeline.log # Auto-generated log file
90
+ ```
91
+
92
+ ---
93
+
94
+ ## API Reference
95
+
96
+ ### CropStressPipeline Class
97
+
98
+ #### `__init__(config_path: str = None)`
99
+ Initialize pipeline with environment configuration.
100
+
101
+ **Args:**
102
+ - `config_path`: Optional path to .env file
103
+
104
+ #### `run(center_lat, center_lon, crop_type, analysis_date, field_size_hectares, farmer_context, output_path=None)`
105
+ Execute complete pipeline.
106
+
107
+ **Args:**
108
+ - `center_lat` (float): Field center latitude
109
+ - `center_lon` (float): Field center longitude
110
+ - `crop_type` (str): Crop type (e.g., 'Wheat', 'Rice', 'Corn')
111
+ - `analysis_date` (str): Target date in 'YYYY-MM-DD' format
112
+ - `field_size_hectares` (float): Field size in hectares
113
+ - `farmer_context` (dict): Farmer profile with keys:
114
+ - `role`: Farmer role
115
+ - `years_farming`: Years of experience
116
+ - `irrigation_method`: Irrigation type
117
+ - `farming_goal`: Primary farming objective
118
+ - `output_path` (str, optional): Path to save JSON results
119
+
120
+ **Returns:**
121
+ - `dict`: Complete analysis results
122
+
123
+ ---
124
+
125
+ ## Output Format
126
+
127
+ The pipeline generates a JSON file with the following structure:
128
+
129
+ ```json
130
+ {
131
+ "metadata": {
132
+ "crop_type": "Wheat",
133
+ "analysis_date": "2024-01-15",
134
+ "location": {"lat": 30.23, "lon": 75.83},
135
+ "field_size_hectares": 0.04,
136
+ "farmer_context": {...},
137
+ "num_images": 10,
138
+ "date_range": ["2023-10-15", "2024-01-15"]
139
+ },
140
+ "vegetation_indices_summary": {
141
+ "indices": {
142
+ "NDVI": {
143
+ "latest": {"mean": 0.65, "std": 0.12},
144
+ "max_in_field": 0.82,
145
+ "min_in_field": 0.41,
146
+ "change": 0.15
147
+ },
148
+ ...
149
+ }
150
+ },
151
+ "stress_detection": {
152
+ "field_statistics": {
153
+ "overall_stress": {"mean": 0.45, "std": 0.15},
154
+ "stress_distribution": {"low": 15, "moderate": 20, "high": 5}
155
+ },
156
+ "cluster_statistics": [
157
+ {
158
+ "cluster_id": 0,
159
+ "percentage": 40.0,
160
+ "stress_score": {"mean": 0.25, "std": 0.05},
161
+ "band_statistics": {...},
162
+ "temporal_trends": {
163
+ "B04": {"change": -0.01, "trend_direction": "stable"},
164
+ "B08": {"change": 0.05, "trend_direction": "increasing"}
165
+ }
166
+ }
167
+ ],
168
+ "anomaly_information": {
169
+ "total_anomalies": 2,
170
+ "anomaly_percentage": 5.0
171
+ }
172
+ },
173
+ "llm_analysis": {
174
+ "soil_moisture": {
175
+ "level": "moderate",
176
+ "maximum_value": 0.65,
177
+ "minimum_value": 0.32,
178
+ "analysis": "..."
179
+ },
180
+ "vegetation_stress": {...},
181
+ "overall_health": {
182
+ "status": "good",
183
+ "key_concerns": ["..."],
184
+ "recommendations": ["..."]
185
+ }
186
+ }
187
+ }
188
+ ```
189
+
190
+ ---
191
+
192
+ ## Logging
193
+
194
+ All pipeline operations are logged to:
195
+ - **Console**: Real-time progress
196
+ - **File**: `crop_stress_pipeline.log`
197
+
198
+ **Log Levels:**
199
+ - `INFO`: Normal operations
200
+ - `ERROR`: Failures and exceptions
201
+
202
+ **Example Log Output:**
203
+ ```
204
+ 2024-12-04 16:45:00 - INFO - Pipeline initialized successfully
205
+ 2024-12-04 16:45:05 - INFO - Fetching satellite data...
206
+ 2024-12-04 16:45:30 - INFO - Found 10 suitable images
207
+ 2024-12-04 16:46:00 - INFO - Calculated 13 indices
208
+ 2024-12-04 16:46:15 - INFO - Stress detection complete
209
+ 2024-12-04 16:46:30 - INFO - LLM analysis complete
210
+ 2024-12-04 16:46:35 - INFO - PIPELINE COMPLETED SUCCESSFULLY
211
+ ```
212
+
213
+ ---
214
+
215
+ ## Configuration Parameters
216
+
217
+ ### Clustering
218
+ - **Number of clusters**: Fixed at 3 (low, moderate, high stress)
219
+ - **Contamination**: 0.1 (10% expected anomalies)
220
+
221
+ ### Spatial Resolution
222
+ - **Default**: 10m per pixel
223
+ - **Patch size**: 8x8 pixels
224
+ - **Stride**: 4 pixels (50% overlap)
225
+
226
+ ### Temporal Analysis
227
+ - **Images**: 10 most recent cloud-free images
228
+ - **Search window**: 90 days before + 30 days after target date
229
+ - **Cloud threshold**: < 20%
230
+
231
+ ### Deep Learning Model
232
+ - **Spatial encoder**: CNN (32→64→128 filters)
233
+ - **Temporal encoder**: Bidirectional LSTM (64 units)
234
+ - **Embedding dimensions**: 128
235
+
236
+ ---
237
+
238
+ ## Error Handling
239
+
240
+ Common errors and solutions:
241
+
242
+ ### 1. Missing Credentials
243
+ ```
244
+ ValueError: Sentinel Hub credentials not found
245
+ ```
246
+ **Solution**: Ensure `.env` file exists with valid credentials
247
+
248
+ ### 2. No Images Found
249
+ ```
250
+ IndexError: list index out of range
251
+ ```
252
+ **Solution**: Adjust `analysis_date` or expand search window
253
+
254
+ ### 3. LLM API Error
255
+ ```
256
+ google.api_core.exceptions.PermissionDenied
257
+ ```
258
+ **Solution**: Verify `GEMINI_API_KEY` is valid and has quota
259
+
260
+ ### 4. Insufficient Patches
261
+ ```
262
+ WARNING: Only X patches generated
263
+ ```
264
+ **Solution**: Increase AOI size or reduce `patch_size`
265
+
266
+ ---
267
+
268
+ ## Performance Optimization
269
+
270
+ ### Memory Usage
271
+ - **Typical**: 2-4 GB RAM
272
+ - **Large AOIs**: Consider batch processing
273
+
274
+ ### Processing Time
275
+ - **Small field (0.04 ha)**: ~2-3 minutes
276
+ - **Medium field (1 ha)**: ~5-10 minutes
277
+ - **Large field (10 ha)**: ~20-30 minutes
278
+
279
+ **Bottlenecks:**
280
+ 1. Satellite data download (~30%)
281
+ 2. Deep learning inference (~40%)
282
+ 3. LLM API call (~20%)
283
+ 4. Index calculation (~10%)
284
+
285
+ ---
286
+
287
+ ## Integration Guide
288
+
289
+ ### REST API Wrapper
290
+
291
+ ```python
292
+ from flask import Flask, request, jsonify
293
+ from crop_stress_pipeline import CropStressPipeline
294
+
295
+ app = Flask(__name__)
296
+ pipeline = CropStressPipeline()
297
+
298
+ @app.route('/analyze', methods=['POST'])
299
+ def analyze():
300
+ data = request.json
301
+ try:
302
+ results = pipeline.run(
303
+ center_lat=data['lat'],
304
+ center_lon=data['lon'],
305
+ crop_type=data['crop_type'],
306
+ analysis_date=data['date'],
307
+ field_size_hectares=data['field_size'],
308
+ farmer_context=data['farmer_context']
309
+ )
310
+ return jsonify(results)
311
+ except Exception as e:
312
+ return jsonify({'error': str(e)}), 500
313
+
314
+ if __name__ == '__main__':
315
+ app.run(host='0.0.0.0', port=5000)
316
+ ```
317
+
318
+ ### Batch Processing
319
+
320
+ ```python
321
+ import pandas as pd
322
+
323
+ # Load field data
324
+ fields = pd.read_csv('fields.csv')
325
+
326
+ # Process each field
327
+ for _, field in fields.iterrows():
328
+ results = pipeline.run(
329
+ center_lat=field['lat'],
330
+ center_lon=field['lon'],
331
+ crop_type=field['crop'],
332
+ analysis_date=field['date'],
333
+ field_size_hectares=field['size'],
334
+ farmer_context={...},
335
+ output_path=f"results_{field['id']}.json"
336
+ )
337
+ ```
338
+
339
+ ---
340
+
341
+ ## Troubleshooting
342
+
343
+ ### Enable Debug Logging
344
+
345
+ ```python
346
+ import logging
347
+ logging.getLogger().setLevel(logging.DEBUG)
348
+ ```
349
+
350
+ ### Test Individual Components
351
+
352
+ ```python
353
+ # Test Sentinel Hub connection
354
+ from sentinelhub import SHConfig
355
+ config = SHConfig()
356
+ config.sh_client_id = "your_id"
357
+ config.sh_client_secret = "your_secret"
358
+ # Should not raise errors
359
+
360
+ # Test LLM connection
361
+ import google.generativeai as genai
362
+ genai.configure(api_key="your_key")
363
+ model = genai.GenerativeModel('gemini-flash-latest')
364
+ response = model.generate_content("Hello")
365
+ print(response.text)
366
+ ```
367
+
368
+ ---
369
+
370
+ ## Support
371
+
372
+ For issues or questions:
373
+ 1. Check logs in `crop_stress_pipeline.log`
374
+ 2. Review this guide
375
+ 3. Contact: SIH ML Team
376
+
377
+ ---
378
+
379
+ ## Version History
380
+
381
+ - **v1.0** (2024-12-04): Initial production release
382
+ - 13 vegetation indices
383
+ - CNN+LSTM stress detection
384
+ - Fixed 3-cluster configuration
385
+ - Gemini LLM integration
386
+ - Comprehensive logging
Dockerfile ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install system dependencies
6
+ RUN apt-get update && apt-get install -y \
7
+ build-essential \
8
+ && rm -rf /var/lib/apt/lists/*
9
+
10
+ # Copy requirements first to leverage cache
11
+ COPY requirements.txt .
12
+ RUN pip install --no-cache-dir -r requirements.txt
13
+
14
+ # Copy application code
15
+ COPY . .
16
+
17
+ # Create directory for outputs if needed
18
+ RUN mkdir -p sar_prediction_output && chmod 777 sar_prediction_output
19
+
20
+ # Expose port
21
+ EXPOSE 7860
22
+
23
+ # Run the application
24
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
PACKAGE_SUMMARY.md ADDED
@@ -0,0 +1,305 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PRODUCTION PIPELINE - PACKAGE SUMMARY
2
+
3
+ ## Package Information
4
+
5
+ **Package Name:** Crop Stress Detection Pipeline
6
+ **Version:** 1.0
7
+ **Date Created:** 2024-12-04
8
+ **Total Files:** 11
9
+ **Total Size:** ~100 KB (excluding dependencies)
10
+ **Python Version:** 3.9+
11
+
12
+ ---
13
+
14
+ ## Complete File List
15
+
16
+ ### 1. Core Pipeline Files (5 files)
17
+
18
+ | # | Filename | Size | Description |
19
+ |---|----------|------|-------------|
20
+ | 1 | `crop_stress_pipeline.py` | 19 KB | Main pipeline orchestrator with logging |
21
+ | 2 | `vegetation_indices.py` | 8 KB | 13 vegetation indices calculation |
22
+ | 3 | `stress_detection_preprocessing.py` | 7 KB | Data preprocessing for deep learning |
23
+ | 4 | `stress_detection_model.py` | 19 KB | CNN+LSTM stress detection model |
24
+ | 5 | `llm_analysis.py` | 19 KB | LLM integration and prompt engineering |
25
+
26
+ ### 2. Configuration Files (2 files)
27
+
28
+ | # | Filename | Size | Description |
29
+ |---|----------|------|-------------|
30
+ | 6 | `requirements.txt` | 160 B | Python dependencies (8 packages) |
31
+ | 7 | `.env.template` | 200 B | Environment variables template |
32
+
33
+ ### 3. Documentation Files (3 files)
34
+
35
+ | # | Filename | Size | Description |
36
+ |---|----------|------|-------------|
37
+ | 8 | `README.md` | 11 KB | Package overview and quick start |
38
+ | 9 | `DEVELOPER_GUIDE.md` | 10 KB | Complete developer documentation |
39
+ | 10 | `DEPLOYMENT_CHECKLIST.md` | 5 KB | Deployment guide and checklist |
40
+
41
+ ### 4. Example Files (1 file)
42
+
43
+ | # | Filename | Size | Description |
44
+ |---|----------|------|-------------|
45
+ | 11 | `example_usage.py` | 2 KB | Example usage script |
46
+
47
+ ---
48
+
49
+ ## Key Changes from Notebook Version
50
+
51
+ ### ✅ Removed
52
+ - All visualization code (matplotlib plots)
53
+ - Jupyter notebook cells
54
+ - Interactive displays
55
+ - Clustering optimization function (find_optimal_clusters)
56
+ - Verbose print statements
57
+
58
+ ### ✅ Added
59
+ - Production logging (console + file)
60
+ - Class-based architecture
61
+ - Error handling
62
+ - Comprehensive documentation
63
+ - Example usage scripts
64
+ - Deployment checklist
65
+
66
+ ### ✅ Fixed
67
+ - Clustering: Fixed to k=3 (low, moderate, high stress)
68
+ - Logging: Structured logging for monitoring
69
+ - Output: JSON-only output format
70
+ - Architecture: Maintained full integrity
71
+
72
+ ---
73
+
74
+ ## Dependencies (requirements.txt)
75
+
76
+ ```
77
+ numpy>=1.24.0
78
+ pandas>=2.0.0
79
+ matplotlib>=3.7.0
80
+ scikit-learn>=1.3.0
81
+ tensorflow>=2.13.0
82
+ sentinelhub>=3.9.0
83
+ python-dotenv>=1.0.0
84
+ google-generativeai>=0.3.0
85
+ ```
86
+
87
+ **Total Dependencies:** 8 packages
88
+ **Installation:** `pip install -r requirements.txt`
89
+
90
+ ---
91
+
92
+ ## Environment Variables (.env.template)
93
+
94
+ ```env
95
+ SH_CLIENT_ID=your_client_id_here
96
+ SH_CLIENT_SECRET=your_client_secret_here
97
+ GEMINI_API_KEY=your_gemini_api_key_here
98
+ ```
99
+
100
+ **Setup:**
101
+ 1. Copy `.env.template` to `.env`
102
+ 2. Fill in actual credentials
103
+ 3. Never commit `.env` to version control
104
+
105
+ ---
106
+
107
+ ## Usage Example
108
+
109
+ ```python
110
+ from crop_stress_pipeline import CropStressPipeline
111
+
112
+ # Initialize
113
+ pipeline = CropStressPipeline()
114
+
115
+ # Run analysis
116
+ results = pipeline.run(
117
+ center_lat=30.2300,
118
+ center_lon=75.8300,
119
+ crop_type='Wheat',
120
+ analysis_date='2024-01-15',
121
+ field_size_hectares=0.04,
122
+ farmer_context={
123
+ 'role': 'Owner-Operator',
124
+ 'years_farming': 15,
125
+ 'irrigation_method': 'Drip Irrigation',
126
+ 'farming_goal': 'Maximize yield'
127
+ },
128
+ output_path='results.json'
129
+ )
130
+
131
+ # Results saved to results.json
132
+ # Logs saved to crop_stress_pipeline.log
133
+ ```
134
+
135
+ ---
136
+
137
+ ## Output Files
138
+
139
+ ### Generated During Execution
140
+
141
+ 1. **`crop_stress_pipeline.log`** - Execution logs
142
+ 2. **`results.json`** - Analysis results (or custom name)
143
+
144
+ ### Output JSON Structure
145
+
146
+ ```json
147
+ {
148
+ "metadata": {...},
149
+ "vegetation_indices_summary": {...},
150
+ "stress_detection": {
151
+ "field_statistics": {...},
152
+ "cluster_statistics": [
153
+ {
154
+ "cluster_id": 0,
155
+ "temporal_trends": {...}
156
+ }
157
+ ],
158
+ "anomaly_information": {...}
159
+ },
160
+ "llm_analysis": {...}
161
+ }
162
+ ```
163
+
164
+ ---
165
+
166
+ ## Deployment Instructions
167
+
168
+ ### Step 1: Transfer Files
169
+ Copy all 11 files to production server
170
+
171
+ ### Step 2: Install Dependencies
172
+ ```bash
173
+ python -m venv venv
174
+ source venv/bin/activate # Windows: venv\Scripts\activate
175
+ pip install -r requirements.txt
176
+ ```
177
+
178
+ ### Step 3: Configure Environment
179
+ ```bash
180
+ cp .env.template .env
181
+ # Edit .env with actual credentials
182
+ ```
183
+
184
+ ### Step 4: Test
185
+ ```bash
186
+ python example_usage.py
187
+ ```
188
+
189
+ ### Step 5: Integrate
190
+ Use `crop_stress_pipeline.py` in your application
191
+
192
+ ---
193
+
194
+ ## Architecture Integrity
195
+
196
+ ### ✅ Maintained
197
+ - 13 vegetation indices calculation
198
+ - CNN + LSTM spatial-temporal encoding
199
+ - K-Means clustering (k=3)
200
+ - Isolation Forest anomaly detection
201
+ - Temporal trend calculation per cluster
202
+ - LLM prompt with comprehensive context
203
+ - All band statistics and temporal features
204
+
205
+ ### ✅ Configuration
206
+ - **Clusters:** Fixed at 3 (production default)
207
+ - **Patch Size:** 8x8 pixels
208
+ - **Stride:** 4 pixels
209
+ - **Contamination:** 0.1 (10%)
210
+ - **Resolution:** 10m
211
+ - **Images:** 10 cloud-free
212
+
213
+ ---
214
+
215
+ ## What to Send to Developer
216
+
217
+ ### Minimum Package (9 files - Required)
218
+ 1. `crop_stress_pipeline.py`
219
+ 2. `vegetation_indices.py`
220
+ 3. `stress_detection_preprocessing.py`
221
+ 4. `stress_detection_model.py`
222
+ 5. `llm_analysis.py`
223
+ 6. `requirements.txt`
224
+ 7. `.env.template`
225
+ 8. `DEVELOPER_GUIDE.md`
226
+ 9. `DEPLOYMENT_CHECKLIST.md`
227
+
228
+ ### Complete Package (11 files - Recommended)
229
+ All 9 above +
230
+ 10. `README.md`
231
+ 11. `example_usage.py`
232
+
233
+ ---
234
+
235
+ ## Support Documentation
236
+
237
+ | Document | Purpose | Audience |
238
+ |----------|---------|----------|
239
+ | `README.md` | Quick start and overview | All users |
240
+ | `DEVELOPER_GUIDE.md` | Complete API reference | Developers |
241
+ | `DEPLOYMENT_CHECKLIST.md` | Deployment steps | DevOps |
242
+
243
+ ---
244
+
245
+ ## Quality Assurance
246
+
247
+ ### ✅ Code Quality
248
+ - No visualization dependencies
249
+ - Production logging only
250
+ - Clean error handling
251
+ - Type hints included
252
+ - Comprehensive docstrings
253
+
254
+ ### ✅ Documentation
255
+ - Complete API reference
256
+ - Usage examples
257
+ - Deployment guide
258
+ - Troubleshooting section
259
+
260
+ ### ✅ Testing
261
+ - All modules importable
262
+ - Example script provided
263
+ - Logging verified
264
+ - Output format validated
265
+
266
+ ---
267
+
268
+ ## Version Control
269
+
270
+ **Recommended .gitignore:**
271
+ ```
272
+ .env
273
+ *.log
274
+ *.json
275
+ __pycache__/
276
+ *.pyc
277
+ venv/
278
+ ```
279
+
280
+ ---
281
+
282
+ ## Contact
283
+
284
+ For questions or issues:
285
+ 1. Check `DEVELOPER_GUIDE.md`
286
+ 2. Review `crop_stress_pipeline.log`
287
+ 3. Contact: SIH ML Team
288
+
289
+ ---
290
+
291
+ ## Changelog
292
+
293
+ ### v1.0 (2024-12-04)
294
+ - Initial production release
295
+ - Converted from Jupyter notebook
296
+ - Removed all visualizations
297
+ - Added production logging
298
+ - Fixed clustering to k=3
299
+ - Added comprehensive documentation
300
+ - Created deployment checklist
301
+ - Added example usage script
302
+
303
+ ---
304
+
305
+ **Package Ready for Handover to Developer** ✅
README.md CHANGED
@@ -1,10 +1,367 @@
1
- ---
2
- title: S2 Pipeline
3
- emoji: 📚
4
- colorFrom: purple
5
- colorTo: indigo
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Crop Stress Detection Pipeline - Production Package
2
+
3
+ ## Overview
4
+
5
+ Production-ready pipeline for comprehensive crop stress analysis using Sentinel-2 satellite imagery, deep learning, and LLM-powered insights.
6
+
7
+ **Version:** 1.0
8
+ **Date:** 2024-12-04
9
+ **Python:** 3.9+
10
+
11
+ ---
12
+
13
+ ## Features
14
+
15
+ ✅ **13 Vegetation Indices**: NDVI, EVI, NDWI, NDRE, RECI, SMI, NDSI, PRI, PSRI, MCARI, SASI, SOMI, SFI
16
+ ✅ **Temporal Analysis**: Multi-temporal statistics and trend detection
17
+ ✅ **Deep Learning**: CNN + LSTM spatial-temporal encoding
18
+ ✅ **Stress Clustering**: K-Means clustering (k=3: low, moderate, high)
19
+ ✅ **Anomaly Detection**: Isolation Forest for unusual patterns
20
+ ✅ **LLM Analysis**: Gemini-powered comprehensive insights
21
+ ✅ **Production Logging**: Comprehensive logging for monitoring
22
+ ✅ **JSON Output**: Structured results for easy integration
23
+
24
+ ---
25
+
26
+ ## Quick Start
27
+
28
+ ### 1. Install Dependencies
29
+
30
+ ```bash
31
+ pip install -r requirements.txt
32
+ ```
33
+
34
+ ### 2. Configure Environment
35
+
36
+ Copy `.env.template` to `.env` and fill in your credentials:
37
+
38
+ ```env
39
+ SH_CLIENT_ID=your_sentinel_hub_client_id
40
+ SH_CLIENT_SECRET=your_sentinel_hub_secret
41
+ GEMINI_API_KEY=your_gemini_api_key
42
+ ```
43
+
44
+ ### 3. Run Pipeline
45
+
46
+ ```python
47
+ from crop_stress_pipeline import CropStressPipeline
48
+
49
+ pipeline = CropStressPipeline()
50
+
51
+ results = pipeline.run(
52
+ center_lat=30.2300,
53
+ center_lon=75.8300,
54
+ crop_type='Wheat',
55
+ analysis_date='2024-01-15',
56
+ field_size_hectares=0.04,
57
+ farmer_context={
58
+ 'role': 'Owner-Operator',
59
+ 'years_farming': 15,
60
+ 'irrigation_method': 'Drip Irrigation',
61
+ 'farming_goal': 'Maximize yield'
62
+ },
63
+ output_path='results.json'
64
+ )
65
+ ```
66
+
67
+ ---
68
+
69
+ ## Package Contents
70
+
71
+ ### Core Files (9 files)
72
+
73
+ | File | Description | Size |
74
+ |------|-------------|------|
75
+ | `crop_stress_pipeline.py` | Main pipeline orchestrator | ~15 KB |
76
+ | `vegetation_indices.py` | 13 vegetation indices calculation | ~8 KB |
77
+ | `stress_detection_preprocessing.py` | Data preprocessing for DL | ~7 KB |
78
+ | `stress_detection_model.py` | CNN+LSTM stress detection | ~19 KB |
79
+ | `llm_analysis.py` | LLM integration & prompts | ~19 KB |
80
+ | `requirements.txt` | Python dependencies | ~200 B |
81
+ | `.env.template` | Environment variables template | ~150 B |
82
+ | `DEVELOPER_GUIDE.md` | Complete documentation | ~12 KB |
83
+ | `DEPLOYMENT_CHECKLIST.md` | Deployment guide | ~6 KB |
84
+
85
+ ### Optional Files
86
+
87
+ | File | Description |
88
+ |------|-------------|
89
+ | `example_usage.py` | Example usage script |
90
+ | `README.md` | This file |
91
+
92
+ **Total Package Size:** ~90 KB (excluding dependencies)
93
+
94
+ ---
95
+
96
+ ## Architecture
97
+
98
+ ```
99
+ ┌─────────────────────────────────────────────────────────┐
100
+ │ INPUT PARAMETERS │
101
+ │ (lat, lon, crop_type, date, farmer_context) │
102
+ └────────────────────┬────────────────────────────────────┘
103
+
104
+
105
+ ┌─────────────────────────────────────────────────────────┐
106
+ │ STEP 1: SATELLITE DATA ACQUISITION │
107
+ │ • Sentinel Hub API (CDSE) │
108
+ │ • 10 cloud-free images (< 20% cloud cover) │
109
+ │ • 10m resolution, 13 spectral bands │
110
+ └────────────────────┬────────────────────────────────────┘
111
+
112
+
113
+ ┌─────────────────────────────────────────────────────────┐
114
+ │ STEP 2: VEGETATION INDICES CALCULATION │
115
+ │ • 13 indices calculated per pixel per timestamp │
116
+ │ • Temporal statistics (mean, std, trend, rolling avg) │
117
+ └────────────────────┬────────────────────────────────────┘
118
+
119
+
120
+ ┌─────────────────────────────────────────────────────────┐
121
+ │ STEP 3: STRESS DETECTION (DEEP LEARNING) │
122
+ │ • Preprocessing: 8x8 patches, stride=4 │
123
+ │ • Spatial Encoding: CNN (32→64→128 filters) │
124
+ │ • Temporal Encoding: Bidirectional LSTM (64 units) │
125
+ │ • Clustering: K-Means (k=3) │
126
+ │ • Anomaly Detection: Isolation Forest (10% contam.) │
127
+ └────────────────────┬────────────────────────────────────┘
128
+
129
+
130
+ ┌─────────────────────────────────────────────────────────┐
131
+ │ STEP 4: LLM ANALYSIS (GEMINI) │
132
+ │ • Input: Indices + Temporal Stats + Stress Context │
133
+ │ • Output: Soil, stress, fertility, health insights │
134
+ │ • Format: Structured JSON │
135
+ └────────────────────┬────────────────────────────────────┘
136
+
137
+
138
+ ┌─────────────────────────────────────────────────────────┐
139
+ │ JSON OUTPUT FILE │
140
+ │ • Metadata, indices, stress detection, LLM analysis │
141
+ └─────────────────────────────────────────────────────────┘
142
+ ```
143
+
144
+ ---
145
+
146
+ ## Output Format
147
+
148
+ ```json
149
+ {
150
+ "metadata": {
151
+ "crop_type": "Wheat",
152
+ "analysis_date": "2024-01-15",
153
+ "location": {"lat": 30.23, "lon": 75.83},
154
+ "num_images": 10
155
+ },
156
+ "vegetation_indices_summary": {
157
+ "indices": {
158
+ "NDVI": {
159
+ "latest": {"mean": 0.65},
160
+ "change": 0.15
161
+ }
162
+ }
163
+ },
164
+ "stress_detection": {
165
+ "field_statistics": {
166
+ "overall_stress": {"mean": 0.45}
167
+ },
168
+ "cluster_statistics": [
169
+ {
170
+ "cluster_id": 0,
171
+ "stress_score": {"mean": 0.25},
172
+ "temporal_trends": {
173
+ "B08": {"trend_direction": "increasing"}
174
+ }
175
+ }
176
+ ]
177
+ },
178
+ "llm_analysis": {
179
+ "soil_moisture": {"level": "moderate"},
180
+ "overall_health": {"status": "good"}
181
+ }
182
+ }
183
+ ```
184
+
185
+ ---
186
+
187
+ ## System Requirements
188
+
189
+ ### Minimum
190
+ - **CPU**: 2 cores
191
+ - **RAM**: 4 GB
192
+ - **Disk**: 1 GB free space
193
+ - **Python**: 3.9+
194
+ - **Internet**: Required (API calls)
195
+
196
+ ### Recommended
197
+ - **CPU**: 4+ cores
198
+ - **RAM**: 8 GB
199
+ - **Disk**: 5 GB free space
200
+ - **Python**: 3.10+
201
+ - **GPU**: Optional (speeds up DL inference)
202
+
203
+ ---
204
+
205
+ ## API Credentials
206
+
207
+ ### Sentinel Hub (CDSE)
208
+ - **Register**: https://dataspace.copernicus.eu/
209
+ - **Free Tier**: 30,000 processing units/month
210
+ - **Usage**: ~100 PU per field analysis
211
+
212
+ ### Google Gemini
213
+ - **Get Key**: https://makersuite.google.com/app/apikey
214
+ - **Free Tier**: 60 requests/minute, 1500/day
215
+ - **Usage**: 1 request per field analysis
216
+
217
+ ---
218
+
219
+ ## Performance
220
+
221
+ | Field Size | Processing Time | Memory Usage |
222
+ |------------|----------------|--------------|
223
+ | 0.04 ha (small) | 2-3 minutes | 2-3 GB |
224
+ | 1 ha (medium) | 5-10 minutes | 3-5 GB |
225
+ | 10 ha (large) | 20-30 minutes | 6-8 GB |
226
+
227
+ **Bottlenecks:**
228
+ 1. Satellite data download (30%)
229
+ 2. Deep learning inference (40%)
230
+ 3. LLM API call (20%)
231
+ 4. Index calculation (10%)
232
+
233
+ ---
234
+
235
+ ## Configuration
236
+
237
+ ### Fixed Parameters (Production)
238
+ - **Clusters**: 3 (low, moderate, high stress)
239
+ - **Patch Size**: 8x8 pixels
240
+ - **Stride**: 4 pixels
241
+ - **Contamination**: 0.1 (10% anomalies)
242
+ - **Resolution**: 10m per pixel
243
+ - **Images**: 10 most recent cloud-free
244
+
245
+ ### Customizable Parameters
246
+ - `center_lat`, `center_lon`: Field location
247
+ - `crop_type`: Crop being analyzed
248
+ - `analysis_date`: Target date
249
+ - `field_size_hectares`: Field size
250
+ - `farmer_context`: Farmer profile
251
+
252
+ ---
253
+
254
+ ## Logging
255
+
256
+ All operations logged to:
257
+ - **Console**: Real-time progress
258
+ - **File**: `crop_stress_pipeline.log`
259
+
260
+ **Log Levels:**
261
+ - `INFO`: Normal operations
262
+ - `ERROR`: Failures
263
+
264
+ **Example:**
265
+ ```
266
+ 2024-12-04 18:45:00 - INFO - Pipeline initialized
267
+ 2024-12-04 18:45:30 - INFO - Found 10 suitable images
268
+ 2024-12-04 18:46:00 - INFO - Calculated 13 indices
269
+ 2024-12-04 18:46:30 - INFO - PIPELINE COMPLETED SUCCESSFULLY
270
+ ```
271
+
272
+ ---
273
+
274
+ ## Error Handling
275
+
276
+ Common errors and solutions:
277
+
278
+ | Error | Solution |
279
+ |-------|----------|
280
+ | Missing credentials | Check `.env` file |
281
+ | No images found | Adjust `analysis_date` |
282
+ | LLM API error | Verify `GEMINI_API_KEY` |
283
+ | Out of memory | Reduce AOI size or increase RAM |
284
+
285
+ ---
286
+
287
+ ## Integration Examples
288
+
289
+ ### REST API
290
+
291
+ ```python
292
+ from flask import Flask, request, jsonify
293
+ from crop_stress_pipeline import CropStressPipeline
294
+
295
+ app = Flask(__name__)
296
+ pipeline = CropStressPipeline()
297
+
298
+ @app.route('/analyze', methods=['POST'])
299
+ def analyze():
300
+ data = request.json
301
+ results = pipeline.run(**data)
302
+ return jsonify(results)
303
+ ```
304
+
305
+ ### Batch Processing
306
+
307
+ ```python
308
+ import pandas as pd
309
+
310
+ fields = pd.read_csv('fields.csv')
311
+
312
+ for _, field in fields.iterrows():
313
+ results = pipeline.run(
314
+ center_lat=field['lat'],
315
+ center_lon=field['lon'],
316
+ crop_type=field['crop'],
317
+ analysis_date=field['date'],
318
+ field_size_hectares=field['size'],
319
+ farmer_context={...},
320
+ output_path=f"results_{field['id']}.json"
321
+ )
322
+ ```
323
+
324
+ ---
325
+
326
+ ## Files to Send to Developer
327
+
328
+ **Required (9 files):**
329
+ 1. `crop_stress_pipeline.py`
330
+ 2. `vegetation_indices.py`
331
+ 3. `stress_detection_preprocessing.py`
332
+ 4. `stress_detection_model.py`
333
+ 5. `llm_analysis.py`
334
+ 6. `requirements.txt`
335
+ 7. `.env.template`
336
+ 8. `DEVELOPER_GUIDE.md`
337
+ 9. `DEPLOYMENT_CHECKLIST.md`
338
+
339
+ **Optional:**
340
+ - `example_usage.py`
341
+ - `README.md`
342
+
343
+ ---
344
+
345
+ ## Support
346
+
347
+ - **Documentation**: See `DEVELOPER_GUIDE.md`
348
+ - **Deployment**: See `DEPLOYMENT_CHECKLIST.md`
349
+ - **Logs**: Check `crop_stress_pipeline.log`
350
+
351
+ ---
352
+
353
+ ## License
354
+
355
+ Internal use only - SIH ML Team
356
+
357
+ ---
358
+
359
+ ## Version History
360
+
361
+ - **v1.0** (2024-12-04): Initial production release
362
+ - 13 vegetation indices
363
+ - CNN+LSTM stress detection
364
+ - Fixed 3-cluster configuration
365
+ - Gemini LLM integration
366
+ - Production logging
367
+ - No visualizations (production-ready)
app.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import uvicorn
3
+ from fastapi import FastAPI, HTTPException
4
+ from pydantic import BaseModel
5
+ from typing import Dict, Any, Optional
6
+ from crop_stress_pipeline import CropStressPipeline
7
+
8
+ app = FastAPI(title="Sentinel-2 Crop Stress Pipeline")
9
+
10
+ # Initialize pipeline
11
+ pipeline = CropStressPipeline()
12
+
13
+ class AnalysisRequest(BaseModel):
14
+ center_lat: float
15
+ center_lon: float
16
+ crop_type: str
17
+ analysis_date: str
18
+ field_size_hectares: float
19
+ farmer_context: Dict[str, Any]
20
+
21
+ @app.get("/")
22
+ def home():
23
+ return {"status": "running", "message": "Sentinel-2 Crop Stress Pipeline API"}
24
+
25
+ @app.post("/analyze")
26
+ async def analyze_crop(request: AnalysisRequest):
27
+ try:
28
+ results = pipeline.run(
29
+ center_lat=request.center_lat,
30
+ center_lon=request.center_lon,
31
+ crop_type=request.crop_type,
32
+ analysis_date=request.analysis_date,
33
+ field_size_hectares=request.field_size_hectares,
34
+ farmer_context=request.farmer_context
35
+ )
36
+ return results
37
+ except Exception as e:
38
+ raise HTTPException(status_code=500, detail=str(e))
39
+
40
+ if __name__ == "__main__":
41
+ uvicorn.run(app, host="0.0.0.0", port=7860)
crop_stress_pipeline.py ADDED
@@ -0,0 +1,477 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Crop Stress Detection Pipeline - Production Version
3
+ ====================================================
4
+
5
+ Complete pipeline for crop monitoring using Sentinel-2 satellite data.
6
+ Includes vegetation indices calculation, deep learning stress detection,
7
+ and LLM-powered analysis.
8
+
9
+ Author: SIH ML Team
10
+ Version: 1.0
11
+ """
12
+
13
+ import os
14
+ import sys
15
+ import json
16
+ import logging
17
+ import numpy as np
18
+ from datetime import datetime, timedelta, timezone
19
+ from dotenv import load_dotenv
20
+
21
+ from sentinelhub import (
22
+ SHConfig, BBox, CRS, DataCollection, SentinelHubRequest,
23
+ MimeType, bbox_to_dimensions, SentinelHubCatalog
24
+ )
25
+
26
+ # Import custom modules
27
+ from vegetation_indices import calculate_indices_temporal, get_summary_report, get_temporal_statistics
28
+ from stress_detection_preprocessing import preprocess_for_model
29
+ from stress_detection_model import StressDetectionModel, prepare_llm_context, get_stress_category
30
+ from llm_analysis import analyze_with_llm
31
+
32
+ # Configure logging
33
+ logging.basicConfig(
34
+ level=logging.INFO,
35
+ format='%(asctime)s - %(levelname)s - %(message)s',
36
+ handlers=[
37
+ logging.FileHandler('crop_stress_pipeline.log'),
38
+ logging.StreamHandler()
39
+ ]
40
+ )
41
+ logger = logging.getLogger(__name__)
42
+
43
+
44
+ class CropStressPipeline:
45
+ """
46
+ Production pipeline for crop stress detection and analysis.
47
+ """
48
+
49
+ def __init__(self, config_path: str = None):
50
+ """
51
+ Initialize pipeline with configuration.
52
+
53
+ Args:
54
+ config_path: Path to .env file with credentials
55
+ """
56
+ # Load environment variables
57
+ if config_path:
58
+ load_dotenv(config_path)
59
+ else:
60
+ load_dotenv()
61
+
62
+ # Configure Sentinel Hub
63
+ self.config = SHConfig()
64
+ self.config.sh_client_id = os.environ.get('SH_CLIENT_ID')
65
+ self.config.sh_client_secret = os.environ.get('SH_CLIENT_SECRET')
66
+ self.config.sh_base_url = 'https://sh.dataspace.copernicus.eu'
67
+ self.config.sh_token_url = 'https://identity.dataspace.copernicus.eu/auth/realms/CDSE/protocol/openid-connect/token'
68
+
69
+ if not self.config.sh_client_id or not self.config.sh_client_secret:
70
+ raise ValueError('Sentinel Hub credentials not found in environment variables')
71
+
72
+ # Define custom CDSE data collection
73
+ self.SENTINEL2_L2A_CDSE = DataCollection.define(
74
+ "SENTINEL2_L2A_CDSE",
75
+ api_id="sentinel-2-l2a",
76
+ service_url="https://sh.dataspace.copernicus.eu",
77
+ collection_type="Sentinel-2",
78
+ is_timeless=False
79
+ )
80
+
81
+ logger.info("Pipeline initialized successfully")
82
+ logger.info(f"Sentinel Hub: {self.config.sh_base_url}")
83
+ logger.info(f"Client ID: {self.config.sh_client_id[:20]}...")
84
+
85
+ def create_evalscript(self) -> str:
86
+ """Create evalscript for Sentinel-2 data retrieval."""
87
+ return """
88
+ //VERSION=3
89
+ function setup() {
90
+ return {
91
+ input: [{
92
+ bands: ["B01", "B02", "B03", "B04", "B05", "B06", "B07", "B08", "B8A", "B09", "B11", "B12", "SCL"],
93
+ units: "DN"
94
+ }],
95
+ output: {
96
+ bands: 13,
97
+ sampleType: "FLOAT32"
98
+ }
99
+ };
100
+ }
101
+
102
+ function evaluatePixel(sample) {
103
+ return [
104
+ sample.B01 / 10000,
105
+ sample.B02 / 10000,
106
+ sample.B03 / 10000,
107
+ sample.B04 / 10000,
108
+ sample.B05 / 10000,
109
+ sample.B06 / 10000,
110
+ sample.B07 / 10000,
111
+ sample.B08 / 10000,
112
+ sample.B8A / 10000,
113
+ sample.B09 / 10000,
114
+ sample.B11 / 10000,
115
+ sample.B12 / 10000,
116
+ sample.SCL
117
+ ];
118
+ }
119
+ """
120
+
121
+ def fetch_satellite_data(self, center_lat: float, center_lon: float,
122
+ analysis_date: str, num_images: int = 10,
123
+ resolution: int = 10) -> tuple:
124
+ """
125
+ Fetch Sentinel-2 satellite data for the specified location and date range.
126
+
127
+ Args:
128
+ center_lat: Latitude of field center
129
+ center_lon: Longitude of field center
130
+ analysis_date: Target analysis date (YYYY-MM-DD)
131
+ num_images: Number of temporal images to fetch
132
+ resolution: Spatial resolution in meters
133
+
134
+ Returns:
135
+ Tuple of (all_images, selected_dates, bbox, size)
136
+ """
137
+ logger.info("=" * 60)
138
+ logger.info("FETCHING SATELLITE DATA")
139
+ logger.info("=" * 60)
140
+
141
+ # Create bounding box
142
+ coords_wgs84 = BBox(
143
+ bbox=[center_lon - 0.001, center_lat - 0.001,
144
+ center_lon + 0.001, center_lat + 0.001],
145
+ crs=CRS.WGS84
146
+ )
147
+
148
+ size = bbox_to_dimensions(coords_wgs84, resolution=resolution)
149
+ logger.info(f"AOI size: {size[0]}x{size[1]} pixels at {resolution}m resolution")
150
+
151
+ # Search for cloud-free images
152
+ target_date = datetime.strptime(analysis_date, '%Y-%m-%d').replace(tzinfo=timezone.utc)
153
+ search_start = target_date - timedelta(days=90)
154
+ search_end = target_date + timedelta(days=30)
155
+
156
+ logger.info(f"Searching for {num_images} cloud-free images...")
157
+ logger.info(f"Date range: {search_start.date()} to {search_end.date()}")
158
+
159
+ catalog = SentinelHubCatalog(config=self.config)
160
+ search_iterator = catalog.search(
161
+ DataCollection.SENTINEL2_L2A,
162
+ bbox=coords_wgs84,
163
+ time=(search_start, search_end),
164
+ filter='eo:cloud_cover < 20'
165
+ )
166
+
167
+ all_timestamps = []
168
+ for item in search_iterator:
169
+ timestamp = item['properties']['datetime']
170
+ cloud_cover = item['properties'].get('eo:cloud_cover', 0)
171
+ all_timestamps.append((timestamp, cloud_cover))
172
+
173
+ # Sort by proximity to target date
174
+ all_timestamps.sort(key=lambda x: abs((datetime.fromisoformat(x[0].replace('Z', '+00:00')) - target_date).days))
175
+ selected_dates = [t[0] for t in all_timestamps[:num_images]]
176
+
177
+ logger.info(f"Found {len(selected_dates)} suitable images:")
178
+ for i, (date, cloud) in enumerate(all_timestamps[:num_images]):
179
+ logger.info(f" [{i+1}] {date[:10]} (Cloud: {cloud:.1f}%)")
180
+
181
+ # Fetch data for all timestamps
182
+ logger.info("Fetching satellite data...")
183
+ all_images = []
184
+ evalscript = self.create_evalscript()
185
+
186
+ for i, date in enumerate(selected_dates):
187
+ request = SentinelHubRequest(
188
+ evalscript=evalscript,
189
+ input_data=[SentinelHubRequest.input_data(
190
+ data_collection=self.SENTINEL2_L2A_CDSE,
191
+ time_interval=(date, date)
192
+ )],
193
+ responses=[SentinelHubRequest.output_response('default', MimeType.TIFF)],
194
+ bbox=coords_wgs84,
195
+ size=size,
196
+ config=self.config
197
+ )
198
+
199
+ data = request.get_data()[0]
200
+ valid_pct = 100 * np.sum(data[:,:,12] > 0) / (data.shape[0] * data.shape[1])
201
+ all_images.append(data)
202
+ logger.info(f" [{i+1}/{len(selected_dates)}] {date[:10]} - {valid_pct:.1f}% valid pixels")
203
+
204
+ all_images = np.array(all_images)
205
+ logger.info(f"Data shape: {all_images.shape} (time, height, width, bands)")
206
+ logger.info("=" * 60)
207
+
208
+ return all_images, selected_dates, coords_wgs84, size
209
+
210
+ def calculate_vegetation_indices(self, all_images: np.ndarray,
211
+ selected_dates: list) -> tuple:
212
+ """
213
+ Calculate all 13 vegetation indices and temporal statistics.
214
+
215
+ Args:
216
+ all_images: Satellite imagery array
217
+ selected_dates: List of image dates
218
+
219
+ Returns:
220
+ Tuple of (indices_data, summary_report, temporal_stats)
221
+ """
222
+ logger.info("=" * 60)
223
+ logger.info("CALCULATING VEGETATION INDICES")
224
+ logger.info("=" * 60)
225
+
226
+ indices_data = calculate_indices_temporal(all_images)
227
+ logger.info(f"Calculated {len(indices_data)} indices:")
228
+ for index_name in indices_data.keys():
229
+ logger.info(f" - {index_name}")
230
+
231
+ summary_report = get_summary_report(indices_data, selected_dates)
232
+ temporal_stats = get_temporal_statistics(indices_data)
233
+
234
+ logger.info("\nSummary Statistics:")
235
+ logger.info(f"Analysis Period: {selected_dates[0][:10]} to {selected_dates[-1][:10]}")
236
+ logger.info(f"Number of Images: {summary_report['num_images']}")
237
+
238
+ for index_name, stats in summary_report['indices'].items():
239
+ logger.info(f"\n{index_name}:")
240
+ logger.info(f" Latest Mean: {stats['latest']['mean']:.4f}")
241
+ logger.info(f" Max in Field: {stats['max_in_field']:.4f}")
242
+ logger.info(f" Min in Field: {stats['min_in_field']:.4f}")
243
+ logger.info(f" Temporal Change: {stats['change']:+.4f}")
244
+
245
+ logger.info("=" * 60)
246
+ return indices_data, summary_report, temporal_stats
247
+
248
+ def run_stress_detection(self, all_images: np.ndarray,
249
+ selected_dates: list,
250
+ n_clusters: int = 3) -> tuple:
251
+ """
252
+ Run deep learning stress detection pipeline.
253
+
254
+ Args:
255
+ all_images: Satellite imagery array
256
+ selected_dates: List of image dates
257
+ n_clusters: Number of stress clusters (default: 3)
258
+
259
+ Returns:
260
+ Tuple of (stress_results, stress_llm_context, patches, patch_coords, metadata)
261
+ """
262
+ logger.info("=" * 60)
263
+ logger.info("STRESS DETECTION PIPELINE")
264
+ logger.info("=" * 60)
265
+
266
+ # Preprocess data
267
+ logger.info("Preprocessing data for stress detection...")
268
+ patches, patch_coords, metadata = preprocess_for_model(
269
+ all_images,
270
+ patch_size=8,
271
+ stride=4
272
+ )
273
+
274
+ logger.info(f"Original shape: {metadata['original_shape']}")
275
+ logger.info(f"Selected bands: {metadata['selected_bands']}")
276
+ logger.info(f"Number of patches: {metadata['num_patches']}")
277
+ logger.info(f"Patch shape: {patches.shape}")
278
+
279
+ # Build and run stress detection model
280
+ logger.info("Building stress detection model...")
281
+ stress_model = StressDetectionModel(
282
+ patch_size=metadata['patch_size'],
283
+ num_bands=metadata['num_bands'],
284
+ num_timestamps=len(selected_dates),
285
+ spatial_embedding_dim=128,
286
+ temporal_embedding_dim=128
287
+ )
288
+
289
+ logger.info(f"Running stress detection with {n_clusters} clusters...")
290
+ stress_results = stress_model.predict(patches, n_clusters=n_clusters, contamination=0.1)
291
+
292
+ logger.info("\nStress Detection Results:")
293
+ logger.info(f" Spatial embeddings: {stress_results['spatial_embeddings'].shape}")
294
+ logger.info(f" Temporal embeddings: {stress_results['temporal_embeddings'].shape}")
295
+ logger.info(f" Stress scores: min={stress_results['stress_scores'].min():.3f}, "
296
+ f"max={stress_results['stress_scores'].max():.3f}, "
297
+ f"mean={stress_results['stress_scores'].mean():.3f}")
298
+ logger.info(f" Clusters: {n_clusters}")
299
+ logger.info(f" Anomalies: {np.sum(stress_results['anomaly_labels'] == -1)}")
300
+
301
+ # Prepare context for LLM
302
+ logger.info("Preparing stress detection context for LLM...")
303
+ stress_llm_context = prepare_llm_context(
304
+ stress_results,
305
+ patch_coords,
306
+ patches,
307
+ metadata
308
+ )
309
+
310
+ # Log stress distribution
311
+ stress_categories = [get_stress_category(score) for score in stress_results['stress_scores']]
312
+ unique_categories, counts = np.unique(stress_categories, return_counts=True)
313
+
314
+ logger.info("\nStress Category Distribution:")
315
+ for category, count in zip(unique_categories, counts):
316
+ pct = 100 * count / len(stress_categories)
317
+ logger.info(f" {category}: {count} patches ({pct:.1f}%)")
318
+
319
+ logger.info(f"\nOverall Field Stress Score: {stress_results['stress_scores'].mean():.3f}")
320
+ logger.info(f"Field Stress Category: {get_stress_category(stress_results['stress_scores'].mean())}")
321
+ logger.info("=" * 60)
322
+
323
+ return stress_results, stress_llm_context, patches, patch_coords, metadata
324
+
325
+ def run_llm_analysis(self, summary_report: dict, temporal_stats: dict,
326
+ stress_llm_context: dict, crop_type: str,
327
+ farmer_context: dict, center_lat: float,
328
+ center_lon: float, field_size_hectares: float) -> dict:
329
+ """
330
+ Run LLM analysis on vegetation indices and stress detection results.
331
+
332
+ Args:
333
+ summary_report: Vegetation indices summary
334
+ temporal_stats: Temporal statistics
335
+ stress_llm_context: Stress detection context
336
+ crop_type: Type of crop
337
+ farmer_context: Farmer profile information
338
+ center_lat: Field latitude
339
+ center_lon: Field longitude
340
+ field_size_hectares: Field size in hectares
341
+
342
+ Returns:
343
+ LLM analysis results dictionary
344
+ """
345
+ logger.info("=" * 60)
346
+ logger.info("LLM ANALYSIS")
347
+ logger.info("=" * 60)
348
+
349
+ logger.info("Analyzing with LLM (Gemini)...")
350
+ llm_analysis = analyze_with_llm(
351
+ summary_report=summary_report,
352
+ crop_type=crop_type,
353
+ farmer_context=farmer_context,
354
+ center_lat=center_lat,
355
+ center_lon=center_lon,
356
+ field_size_hectares=field_size_hectares,
357
+ temporal_stats=temporal_stats,
358
+ stress_context=stress_llm_context
359
+ )
360
+
361
+ logger.info("\nLLM Analysis Results:")
362
+ logger.info(f" Soil Moisture: {llm_analysis['soil_moisture']['level']}")
363
+ logger.info(f" Soil Salinity: {llm_analysis['soil_salinity']['level']}")
364
+ logger.info(f" Organic Matter: {llm_analysis['organic_matter']['level']}")
365
+ logger.info(f" Soil Fertility: {llm_analysis['soil_fertility']['level']}")
366
+ logger.info(f" Vegetation Stress: {llm_analysis['vegetation_stress']['level']}")
367
+ logger.info(f" Photosynthetic Stress: {llm_analysis['photosynthetic_stress']['level']}")
368
+ logger.info(f" Overall Health: {llm_analysis['overall_health']['status']}")
369
+ logger.info("=" * 60)
370
+
371
+ return llm_analysis
372
+
373
+ def run(self, center_lat: float, center_lon: float, crop_type: str,
374
+ analysis_date: str, field_size_hectares: float,
375
+ farmer_context: dict, output_path: str = None) -> dict:
376
+ """
377
+ Run complete crop stress detection pipeline.
378
+
379
+ Args:
380
+ center_lat: Field center latitude
381
+ center_lon: Field center longitude
382
+ crop_type: Type of crop being analyzed
383
+ analysis_date: Target analysis date (YYYY-MM-DD)
384
+ field_size_hectares: Field size in hectares
385
+ farmer_context: Dictionary with farmer profile information
386
+ output_path: Path to save results JSON (optional)
387
+
388
+ Returns:
389
+ Complete analysis results dictionary
390
+ """
391
+ logger.info("\n" + "=" * 60)
392
+ logger.info("CROP STRESS DETECTION PIPELINE - STARTING")
393
+ logger.info("=" * 60)
394
+ logger.info(f"Crop Type: {crop_type}")
395
+ logger.info(f"Analysis Date: {analysis_date}")
396
+ logger.info(f"Location: ({center_lat:.4f}, {center_lon:.4f})")
397
+ logger.info(f"Field Size: {field_size_hectares} hectares")
398
+ logger.info("=" * 60)
399
+
400
+ try:
401
+ # Step 1: Fetch satellite data
402
+ all_images, selected_dates, bbox, size = self.fetch_satellite_data(
403
+ center_lat, center_lon, analysis_date
404
+ )
405
+
406
+ # Step 2: Calculate vegetation indices
407
+ indices_data, summary_report, temporal_stats = self.calculate_vegetation_indices(
408
+ all_images, selected_dates
409
+ )
410
+
411
+ # Step 3: Run stress detection
412
+ stress_results, stress_llm_context, patches, patch_coords, metadata = self.run_stress_detection(
413
+ all_images, selected_dates, n_clusters=3
414
+ )
415
+
416
+ # Step 4: Run LLM analysis
417
+ llm_analysis = self.run_llm_analysis(
418
+ summary_report, temporal_stats, stress_llm_context,
419
+ crop_type, farmer_context, center_lat, center_lon,
420
+ field_size_hectares
421
+ )
422
+
423
+ # Compile results
424
+ results = {
425
+ 'metadata': {
426
+ 'crop_type': crop_type,
427
+ 'analysis_date': analysis_date,
428
+ 'location': {'lat': center_lat, 'lon': center_lon},
429
+ 'field_size_hectares': field_size_hectares,
430
+ 'farmer_context': farmer_context,
431
+ 'num_images': len(selected_dates),
432
+ 'date_range': [selected_dates[0][:10], selected_dates[-1][:10]]
433
+ },
434
+ 'vegetation_indices_summary': summary_report,
435
+ 'stress_detection': stress_llm_context,
436
+ 'llm_analysis': llm_analysis
437
+ }
438
+
439
+ # Save results
440
+ if output_path:
441
+ with open(output_path, 'w') as f:
442
+ json.dump(results, f, indent=2)
443
+ logger.info(f"\nResults saved to: {output_path}")
444
+
445
+ logger.info("\n" + "=" * 60)
446
+ logger.info("PIPELINE COMPLETED SUCCESSFULLY")
447
+ logger.info("=" * 60)
448
+
449
+ return results
450
+
451
+ except Exception as e:
452
+ logger.error(f"Pipeline failed with error: {str(e)}", exc_info=True)
453
+ raise
454
+
455
+
456
+ if __name__ == "__main__":
457
+ # Example usage
458
+ pipeline = CropStressPipeline()
459
+
460
+ # Define field parameters
461
+ params = {
462
+ 'center_lat': 30.2300,
463
+ 'center_lon': 75.8300,
464
+ 'crop_type': 'Wheat',
465
+ 'analysis_date': '2024-01-15',
466
+ 'field_size_hectares': 0.04,
467
+ 'farmer_context': {
468
+ 'role': 'Owner-Operator',
469
+ 'years_farming': 15,
470
+ 'irrigation_method': 'Drip Irrigation',
471
+ 'farming_goal': 'Maximize yield while maintaining soil health'
472
+ },
473
+ 'output_path': 'crop_analysis_results.json'
474
+ }
475
+
476
+ # Run pipeline
477
+ results = pipeline.run(**params)
example_usage.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Example usage script for Crop Stress Detection Pipeline
3
+ """
4
+
5
+ from crop_stress_pipeline import CropStressPipeline
6
+ import json
7
+
8
+ def main():
9
+ """
10
+ Example: Analyze a wheat field in Punjab, India
11
+ """
12
+
13
+ # Initialize pipeline
14
+ print("Initializing pipeline...")
15
+ pipeline = CropStressPipeline()
16
+
17
+ # Define field parameters
18
+ field_params = {
19
+ 'center_lat': 30.2300,
20
+ 'center_lon': 75.8300,
21
+ 'crop_type': 'Wheat',
22
+ 'analysis_date': '2024-01-15',
23
+ 'field_size_hectares': 0.04,
24
+ 'farmer_context': {
25
+ 'role': 'Owner-Operator',
26
+ 'years_farming': 15,
27
+ 'irrigation_method': 'Drip Irrigation',
28
+ 'farming_goal': 'Maximize yield while maintaining soil health'
29
+ },
30
+ 'output_path': 'example_results.json'
31
+ }
32
+
33
+ print("\nField Information:")
34
+ print(f" Location: ({field_params['center_lat']}, {field_params['center_lon']})")
35
+ print(f" Crop: {field_params['crop_type']}")
36
+ print(f" Date: {field_params['analysis_date']}")
37
+ print(f" Size: {field_params['field_size_hectares']} hectares")
38
+
39
+ # Run pipeline
40
+ print("\nRunning pipeline...")
41
+ try:
42
+ results = pipeline.run(**field_params)
43
+
44
+ print("\n" + "="*60)
45
+ print("PIPELINE COMPLETED SUCCESSFULLY")
46
+ print("="*60)
47
+
48
+ # Display key results
49
+ print("\nKey Results:")
50
+ print(f" Overall Health: {results['llm_analysis']['overall_health']['status'].upper()}")
51
+ print(f" Soil Moisture: {results['llm_analysis']['soil_moisture']['level'].upper()}")
52
+ print(f" Vegetation Stress: {results['llm_analysis']['vegetation_stress']['level'].upper()}")
53
+
54
+ print(f"\nFull results saved to: {field_params['output_path']}")
55
+
56
+ # Pretty print a sample of the results
57
+ print("\nSample Output (Vegetation Indices):")
58
+ ndvi_stats = results['vegetation_indices_summary']['indices']['NDVI']
59
+ print(f" NDVI Latest Mean: {ndvi_stats['latest']['mean']:.4f}")
60
+ print(f" NDVI Change: {ndvi_stats['change']:+.4f}")
61
+
62
+ return results
63
+
64
+ except Exception as e:
65
+ print(f"\nERROR: Pipeline failed - {str(e)}")
66
+ print("Check crop_stress_pipeline.log for details")
67
+ raise
68
+
69
+
70
+ if __name__ == "__main__":
71
+ results = main()
llm_analysis.py ADDED
@@ -0,0 +1,435 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ LLM Integration for Vegetation Indices Analysis
3
+ ================================================
4
+
5
+ This module integrates with Google Gemini to analyze vegetation indices
6
+ and provide comprehensive soil and crop insights.
7
+ """
8
+
9
+ import os
10
+ import json
11
+ import numpy as np
12
+ import google.generativeai as genai
13
+ from typing import Dict, Any
14
+
15
+ def configure_gemini():
16
+ """Configure Gemini API with key from environment."""
17
+ api_key = os.environ.get("GEMINI_API_KEY")
18
+ if not api_key:
19
+ raise ValueError("GEMINI_API_KEY not found in environment variables")
20
+ genai.configure(api_key=api_key, transport='rest')
21
+ return genai.GenerativeModel('gemini-flash-latest')
22
+
23
+ def prepare_indices_context(summary_report: Dict, crop_type: str, farmer_context: Dict,
24
+ temporal_stats: Dict = None) -> str:
25
+ """
26
+ Prepare a comprehensive context string for the LLM including temporal statistics.
27
+
28
+ Args:
29
+ summary_report: Dictionary with all indices data
30
+ crop_type: Type of crop being analyzed
31
+ farmer_context: Farmer profile information
32
+ temporal_stats: Dictionary with temporal statistics (optional)
33
+
34
+ Returns:
35
+ Formatted context string
36
+ """
37
+ context = f"""
38
+ CROP MONITORING ANALYSIS REQUEST
39
+
40
+ CROP INFORMATION:
41
+ - Crop Type: {crop_type}
42
+ - Analysis Period: {summary_report['dates'][0]} to {summary_report['dates'][-1]}
43
+ - Number of Images Analyzed: {summary_report['num_images']}
44
+
45
+ FARMER CONTEXT:
46
+ - Role: {farmer_context['role']}
47
+ - Experience: {farmer_context['years_farming']} years
48
+ - Irrigation Method: {farmer_context['irrigation_method']}
49
+ - Farming Goal: {farmer_context['farming_goal']}
50
+
51
+ VEGETATION INDICES DATA (ALL 13 INDICES):
52
+ """
53
+
54
+ for index_name, stats in summary_report['indices'].items():
55
+ context += f"\n{index_name}:"
56
+ context += f"\n - Latest Mean Value: {stats['latest']['mean']:.4f}"
57
+ context += f"\n - Maximum in Field: {stats['max_in_field']:.4f}"
58
+ context += f"\n - Minimum in Field: {stats['min_in_field']:.4f}"
59
+ context += f"\n - Temporal Change (Latest - Oldest): {stats['change']:+.4f}"
60
+ context += f"\n - Temporal Trend (All Values): {stats['mean_values_over_time']}"
61
+
62
+
63
+ # Add temporal statistics if provided
64
+ if temporal_stats:
65
+ context += "\n\nTEMPORAL STATISTICS (FEATURE ENGINEERING):\n"
66
+ for index_name, t_stats in temporal_stats.items():
67
+ context += f"\n{index_name} Temporal Features:"
68
+
69
+ # Mean and std over time
70
+ mean_spatial = float(np.nanmean(t_stats['mean_over_time']))
71
+ std_spatial = float(np.nanmean(t_stats['std_over_time']))
72
+ context += f"\n - Spatial Mean (averaged over time): {mean_spatial:.4f}"
73
+ context += f"\n - Spatial Std (averaged over time): {std_spatial:.4f}"
74
+
75
+ # Max and min over time
76
+ max_val = float(np.nanmax(t_stats['max_over_time']))
77
+ min_val = float(np.nanmin(t_stats['min_over_time']))
78
+ range_val = float(np.nanmean(t_stats['range']))
79
+ context += f"\n - Maximum Value Over Time: {max_val:.4f}"
80
+ context += f"\n - Minimum Value Over Time: {min_val:.4f}"
81
+ context += f"\n - Average Range (Max-Min): {range_val:.4f}"
82
+
83
+ # Temporal trend
84
+ trend_mean = float(np.nanmean(t_stats['temporal_trend']))
85
+ context += f"\n - Average Temporal Trend: {trend_mean:+.4f}"
86
+
87
+ # Rolling average if available
88
+ if 'rolling_avg_3' in t_stats:
89
+ latest_rolling = float(np.nanmean(t_stats['rolling_avg_3'][-1]))
90
+ context += f"\n - Latest Rolling Average (3-period): {latest_rolling:.4f}"
91
+
92
+ return context
93
+
94
+ def format_stress_context(stress_context: Dict) -> str:
95
+ """
96
+ Format stress detection results for LLM prompt.
97
+
98
+ Args:
99
+ stress_context: Dictionary with stress detection results
100
+
101
+ Returns:
102
+ Formatted string with stress patterns, clusters, and anomalies
103
+ """
104
+ if not stress_context:
105
+ return ""
106
+
107
+ c = "\nDEEP LEARNING STRESS DETECTION RESULTS:\n"
108
+ c += "=======================================\n"
109
+
110
+ # Field Statistics
111
+ fs = stress_context.get('field_statistics', {})
112
+ c += f"Overall Field Stress Score: {fs.get('overall_stress', {}).get('mean', 0):.3f} (0=Healthy, 1=Severe Stress)\n"
113
+ c += f"Stress Category Distribution: {fs.get('stress_distribution', {})}\n"
114
+
115
+ # Cluster Statistics (Patterns)
116
+ c += "\nIDENTIFIED CLUSTERING PATTERNS (SPATIAL-TEMPORAL BEHAVIOR):\n"
117
+ for cluster in stress_context.get('cluster_statistics', []):
118
+ c += f" * Cluster {cluster['cluster_id']} ({cluster['percentage']:.1f}% of field):\n"
119
+ c += f" - Average Stress Score: {cluster['stress_score']['mean']:.3f}\n"
120
+ c += f" - Stress Variability (Std): {cluster['stress_score']['std']:.3f}\n"
121
+ # Add key band stats if available to explain *why* it's a cluster
122
+ if 'band_statistics' in cluster:
123
+ c += " - Key Spectral Characteristics:\n"
124
+ # Just show a few key bands to keep it concise
125
+ for band in ['B04', 'B08', 'B11']: # Red, NIR, SWIR
126
+ if band in cluster['band_statistics']:
127
+ val = cluster['band_statistics'][band]['mean']
128
+ c += f" {band}: {val:.4f}\n"
129
+
130
+ # Add temporal trends if available
131
+ if 'temporal_trends' in cluster:
132
+ c += " - Temporal Trends (Change over analysis period):\n"
133
+ for band in ['B04', 'B08', 'B11']: # Red, NIR, SWIR
134
+ if band in cluster['temporal_trends']:
135
+ trend = cluster['temporal_trends'][band]
136
+ c += f" {band}: {trend['trend_direction']} ({trend['change']:+.4f})\n"
137
+
138
+ # Anomaly Information
139
+ anom = stress_context.get('anomaly_information', {})
140
+ c += f"\nANOMALY DETECTION (UNUSUAL PATTERNS):\n"
141
+ c += f"- Total Anomalies Detected: {anom.get('total_anomalies', 0)} patches ({anom.get('anomaly_percentage', 0):.1f}% of field)\n"
142
+ if anom.get('anomaly_patches'):
143
+ c += "- Sample Anomalies:\n"
144
+ for p in anom['anomaly_patches'][:3]:
145
+ c += f" * Patch at {p['coordinates']}: Stress={p['stress_score']:.3f}, Category={p['stress_category']}\n"
146
+
147
+ return c
148
+
149
+ def analyze_with_llm(summary_report: Dict, crop_type: str, farmer_context: Dict,
150
+ center_lat: float, center_lon: float, field_size_hectares: float,
151
+ temporal_stats: Dict = None, stress_context: Dict = None) -> Dict[str, Any]:
152
+ """
153
+ Analyze vegetation indices using Gemini LLM and extract soil insights.
154
+
155
+ Args:
156
+ summary_report: Dictionary with all indices data
157
+ crop_type: Type of crop
158
+ farmer_context: Farmer profile information
159
+ center_lat: Latitude
160
+ center_lon: Longitude
161
+ field_size_hectares: Field size
162
+ temporal_stats: Dictionary with temporal statistics
163
+ stress_context: Dictionary with stress detection results (clustering, anomalies)
164
+
165
+ Returns:
166
+ Dictionary with structured LLM analysis results
167
+ """
168
+ model = configure_gemini()
169
+
170
+ # Prepare context with temporal statistics
171
+ indices_context = prepare_indices_context(summary_report, crop_type, farmer_context, temporal_stats)
172
+
173
+ # Prepare stress context
174
+ stress_text = format_stress_context(stress_context)
175
+
176
+ # Create prompt for LLM
177
+ prompt = f"""
178
+ {indices_context}
179
+
180
+ {stress_text}
181
+
182
+ FIELD METADATA:
183
+ - Location: Latitude {center_lat:.4f}, Longitude {center_lon:.4f}
184
+ - Field Size: {field_size_hectares:.2f} hectares
185
+
186
+ Based on the vegetation indices data AND the deep learning stress detection results above,
187
+ provide a comprehensive analysis.
188
+
189
+ Use the cluster patterns to identify distinct zones in the field.
190
+ Use the anomaly detection results to pinpoint specific problem areas.
191
+ Analyze the temporal trends in each cluster to determine if stress is worsening or recovering.
192
+ Combine the spectral indices (NDVI, NDWI, etc.) with the stress scores to explain the *cause* of stress.
193
+
194
+ You MUST respond with a valid JSON object (no markdown, no code blocks) with EXACTLY this structure:
195
+
196
+ {{
197
+ "soil_moisture": {{
198
+ "level": "low" or "moderate" or "high",
199
+ "maximum_value": <float>,
200
+ "minimum_value": <float>,
201
+ "analysis": "Brief explanation of soil moisture status"
202
+ }},
203
+ "soil_salinity": {{
204
+ "level": "low" or "moderate" or "high",
205
+ "trend": "Four word description of overall trend by considering SASI index given",
206
+ "analysis": "Brief explanation of salinity status"
207
+ }},
208
+ "organic_matter": {{
209
+ "level": "low" or "moderate" or "high",
210
+ "status": "Four word analysis based on SOMI index values",
211
+ "analysis": "Brief explanation of organic matter status"
212
+ }},
213
+ "soil_fertility": {{
214
+ "level": "low" or "moderate" or "high",
215
+ "status": "Four words about soil health based on SFI",
216
+ "analysis": "Brief explanation of soil fertility status"
217
+ }},
218
+ "vegetation_stress": {{
219
+ "level": "low" or "moderate" or "high",
220
+ "status": "Four word description not necessarily a sentence",
221
+ "analysis": "Brief explanation based on NDVI, EVI, NDRE trends"
222
+ }},
223
+ "photosynthetic_stress": {{
224
+ "level": "low" or "moderate" or "high",
225
+ "status": "Four word description not necessarily a sentence",
226
+ "analysis": "Brief explanation based on PRI, PSRI, chlorophyll indices"
227
+ }},
228
+ "hotspot_detection": {{
229
+ "description": "Direction and intensity of stress spreading in less than 6 words",
230
+ "analysis": "Brief explanation of spatial stress patterns"
231
+ }},
232
+ "moisture_zones": {{
233
+ "description": "Moisture variation and trend in not more than 6 words",
234
+ "analysis": "Brief explanation of moisture distribution patterns"
235
+ }},
236
+ "overall_health": {{
237
+ "status": "poor" or "fair" or "good" or "excellent",
238
+ "key_concerns": ["concern1", "concern2"],
239
+ "recommendations": ["recommendation1", "recommendation2"]
240
+ }}
241
+ }}
242
+
243
+ IMPORTANT GUIDELINES:
244
+ - For soil_moisture.maximum_value and minimum_value, use the SMI index values from the data
245
+ - For soil_salinity.trend, provide EXACTLY four words describing the trend based on SASI values
246
+ - For organic_matter.status, provide EXACTLY four words based on SOMI index values
247
+ - For soil_fertility.status, provide EXACTLY four words (not a sentence) about soil health based on SFI values
248
+ - For vegetation_stress.status, provide EXACTLY four words based on NDVI, EVI, NDRE temporal patterns
249
+ - For photosynthetic_stress.status, provide EXACTLY four words based on PRI, PSRI, RECI values
250
+ - For hotspot_detection.description, provide LESS THAN 6 words about stress direction and intensity
251
+ - For moisture_zones.description, provide NOT MORE THAN 6 words about moisture variation and trend
252
+ - Use spatial statistics (max, min, range) to identify hotspots and zones
253
+ - Consider temporal trends to detect spreading patterns
254
+ - Base your analysis on the actual index values provided
255
+ - Provide actionable insights relevant to the farmer's context
256
+
257
+ Return ONLY the JSON object, no additional text.
258
+ """
259
+
260
+ # Get LLM response
261
+ response = model.generate_content(prompt)
262
+ response_text = response.text.strip()
263
+
264
+ # Remove markdown code blocks if present
265
+ if response_text.startswith("```"):
266
+ lines = response_text.split("\n")
267
+ response_text = "\n".join(lines[1:-1])
268
+ if response_text.startswith("json"):
269
+ response_text = response_text[4:].strip()
270
+
271
+ # Parse JSON response
272
+ try:
273
+ analysis = json.loads(response_text)
274
+ return analysis
275
+ except json.JSONDecodeError as e:
276
+ print(f"Error parsing LLM response: {e}")
277
+ print(f"Response text: {response_text}")
278
+ # Return fallback structure
279
+ return {
280
+ "soil_moisture": {
281
+ "level": "moderate",
282
+ "maximum_value": summary_report['indices']['SMI']['max_in_field'],
283
+ "minimum_value": summary_report['indices']['SMI']['min_in_field'],
284
+ "analysis": "Unable to parse LLM response"
285
+ },
286
+ "soil_salinity": {
287
+ "level": "moderate",
288
+ "trend": "Unable to determine trend",
289
+ "analysis": "Unable to parse LLM response"
290
+ },
291
+ "organic_matter": {
292
+ "level": "moderate",
293
+ "status": "Unable to determine status",
294
+ "analysis": "Unable to parse LLM response"
295
+ },
296
+ "soil_fertility": {
297
+ "level": "moderate",
298
+ "status": "Unable to determine status",
299
+ "analysis": "Unable to parse LLM response"
300
+ },
301
+ "vegetation_stress": {
302
+ "level": "moderate",
303
+ "status": "Unable to determine status",
304
+ "analysis": "Unable to parse LLM response"
305
+ },
306
+ "photosynthetic_stress": {
307
+ "level": "moderate",
308
+ "status": "Unable to determine status",
309
+ "analysis": "Unable to parse LLM response"
310
+ },
311
+ "hotspot_detection": {
312
+ "description": "Unable to determine",
313
+ "analysis": "Unable to parse LLM response"
314
+ },
315
+ "moisture_zones": {
316
+ "description": "Unable to determine",
317
+ "analysis": "Unable to parse LLM response"
318
+ },
319
+ "overall_health": {
320
+ "status": "fair",
321
+ "key_concerns": ["Analysis unavailable"],
322
+ "recommendations": ["Please review indices manually"]
323
+ }
324
+ }
325
+
326
+ def format_llm_output(analysis: Dict) -> str:
327
+ """
328
+ Format LLM analysis into a readable report.
329
+
330
+ Args:
331
+ analysis: Dictionary with LLM analysis results
332
+
333
+ Returns:
334
+ Formatted string report
335
+ """
336
+ report = """
337
+ ╔════════════════════════════════════════════════════════════════╗
338
+ ║ LLM ANALYSIS - SOIL & CROP INSIGHTS ║
339
+ ╚════════════════════════════════════════════════════════════════╝
340
+
341
+ SOIL MOISTURE ANALYSIS:
342
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━��━━━━━━━━━━
343
+ """
344
+
345
+ sm = analysis['soil_moisture']
346
+ report += f" Level: {sm['level'].upper()}\n"
347
+ report += f" Maximum Value: {sm['maximum_value']:.4f}\n"
348
+ report += f" Minimum Value: {sm['minimum_value']:.4f}\n"
349
+ report += f" Analysis: {sm['analysis']}\n"
350
+
351
+ report += """
352
+ SOIL SALINITY ANALYSIS:
353
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
354
+ """
355
+
356
+ ss = analysis['soil_salinity']
357
+ report += f" Level: {ss['level'].upper()}\n"
358
+ report += f" Overall Trend: {ss['trend']}\n"
359
+ report += f" Analysis: {ss['analysis']}\n"
360
+
361
+ report += """
362
+ ORGANIC MATTER ANALYSIS:
363
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
364
+ """
365
+
366
+ om = analysis['organic_matter']
367
+ report += f" Level: {om['level'].upper()}\n"
368
+ report += f" Status: {om['status']}\n"
369
+ report += f" Analysis: {om['analysis']}\n"
370
+
371
+ report += """
372
+ SOIL FERTILITY ANALYSIS:
373
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
374
+ """
375
+
376
+ sf = analysis['soil_fertility']
377
+ report += f" Level: {sf['level'].upper()}\n"
378
+ report += f" Status: {sf['status']}\n"
379
+ report += f" Analysis: {sf['analysis']}\n"
380
+
381
+ report += """
382
+ VEGETATION STRESS ANALYSIS:
383
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
384
+ """
385
+
386
+ vs = analysis['vegetation_stress']
387
+ report += f" Level: {vs['level'].upper()}\n"
388
+ report += f" Status: {vs['status']}\n"
389
+ report += f" Analysis: {vs['analysis']}\n"
390
+
391
+ report += """
392
+ PHOTOSYNTHETIC STRESS ANALYSIS:
393
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
394
+ """
395
+
396
+ ps = analysis['photosynthetic_stress']
397
+ report += f" Level: {ps['level'].upper()}\n"
398
+ report += f" Status: {ps['status']}\n"
399
+ report += f" Analysis: {ps['analysis']}\n"
400
+
401
+ report += """
402
+ HOTSPOT DETECTION:
403
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
404
+ """
405
+
406
+ hd = analysis['hotspot_detection']
407
+ report += f" Description: {hd['description']}\n"
408
+ report += f" Analysis: {hd['analysis']}\n"
409
+
410
+ report += """
411
+ MOISTURE ZONES:
412
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
413
+ """
414
+
415
+ mz = analysis['moisture_zones']
416
+ report += f" Description: {mz['description']}\n"
417
+ report += f" Analysis: {mz['analysis']}\n"
418
+
419
+ report += """
420
+ OVERALL CROP HEALTH:
421
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
422
+ """
423
+
424
+ oh = analysis['overall_health']
425
+ report += f" Status: {oh['status'].upper()}\n"
426
+ report += f"\n Key Concerns:\n"
427
+ for concern in oh['key_concerns']:
428
+ report += f" • {concern}\n"
429
+ report += f"\n Recommendations:\n"
430
+ for rec in oh['recommendations']:
431
+ report += f" • {rec}\n"
432
+
433
+ report += "\n" + "═" * 64 + "\n"
434
+
435
+ return report
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ numpy>=1.24.0
2
+ pandas>=2.0.0
3
+ matplotlib>=3.7.0
4
+ scikit-learn>=1.3.0
5
+ tensorflow>=2.13.0
6
+ sentinelhub>=3.9.0
7
+ python-dotenv>=1.0.0
8
+ google-generativeai>=0.3.0
9
+ fastapi>=0.100.0
10
+ uvicorn>=0.23.0
11
+ python-multipart>=0.0.6
stress_detection_model.py ADDED
@@ -0,0 +1,475 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Stress Detection Model
3
+ =======================
4
+
5
+ Deep learning model for crop stress detection using spatial-temporal encoding.
6
+ Architecture: Spatial CNN → Temporal LSTM → Clustering → Anomaly Detection
7
+ """
8
+
9
+ import numpy as np
10
+ import tensorflow as tf
11
+ from tensorflow import keras
12
+ from tensorflow.keras import layers
13
+ from sklearn.cluster import KMeans
14
+ from sklearn.ensemble import IsolationForest
15
+ from sklearn.preprocessing import StandardScaler
16
+ from sklearn.metrics import silhouette_score
17
+ from typing import Tuple, Dict, List
18
+ import warnings
19
+ warnings.filterwarnings('ignore')
20
+
21
+
22
+ class SpatialEncoder(keras.Model):
23
+ """
24
+ CNN-based spatial feature extractor.
25
+ Processes each timestamp independently to extract spatial features.
26
+ """
27
+
28
+ def __init__(self, embedding_dim=128):
29
+ super(SpatialEncoder, self).__init__()
30
+
31
+ # Convolutional layers
32
+ self.conv1 = layers.Conv2D(32, (3, 3), activation='relu', padding='same')
33
+ self.bn1 = layers.BatchNormalization()
34
+ self.pool1 = layers.MaxPooling2D((2, 2))
35
+ self.dropout1 = layers.Dropout(0.25)
36
+
37
+ self.conv2 = layers.Conv2D(64, (3, 3), activation='relu', padding='same')
38
+ self.bn2 = layers.BatchNormalization()
39
+ self.pool2 = layers.MaxPooling2D((2, 2))
40
+ self.dropout2 = layers.Dropout(0.25)
41
+
42
+ self.conv3 = layers.Conv2D(128, (3, 3), activation='relu', padding='same')
43
+ self.bn3 = layers.BatchNormalization()
44
+
45
+ # Global pooling and dense layers
46
+ self.global_pool = layers.GlobalAveragePooling2D()
47
+ self.dense1 = layers.Dense(256, activation='relu')
48
+ self.dropout3 = layers.Dropout(0.3)
49
+ self.dense2 = layers.Dense(embedding_dim, activation='relu')
50
+
51
+ def call(self, x, training=False):
52
+ # x shape: (batch, height, width, channels)
53
+ x = self.conv1(x)
54
+ x = self.bn1(x, training=training)
55
+ x = self.pool1(x)
56
+ x = self.dropout1(x, training=training)
57
+
58
+ x = self.conv2(x)
59
+ x = self.bn2(x, training=training)
60
+ x = self.pool2(x)
61
+ x = self.dropout2(x, training=training)
62
+
63
+ x = self.conv3(x)
64
+ x = self.bn3(x, training=training)
65
+
66
+ x = self.global_pool(x)
67
+ x = self.dense1(x)
68
+ x = self.dropout3(x, training=training)
69
+ x = self.dense2(x)
70
+
71
+ return x # (batch, embedding_dim)
72
+
73
+
74
+ class TemporalEncoder(keras.Model):
75
+ """
76
+ LSTM-based temporal feature extractor.
77
+ Processes sequence of spatial embeddings to capture temporal patterns.
78
+ """
79
+
80
+ def __init__(self, embedding_dim=128, lstm_units=64):
81
+ super(TemporalEncoder, self).__init__()
82
+
83
+ self.lstm = layers.Bidirectional(
84
+ layers.LSTM(lstm_units, return_sequences=False, dropout=0.2)
85
+ )
86
+ self.dense = layers.Dense(embedding_dim, activation='relu')
87
+
88
+ def call(self, x, training=False):
89
+ # x shape: (batch, time, spatial_embedding_dim)
90
+ x = self.lstm(x, training=training)
91
+ x = self.dense(x)
92
+ return x # (batch, embedding_dim)
93
+
94
+
95
+ class StressDetectionModel:
96
+ """
97
+ Complete stress detection pipeline with spatial-temporal encoding,
98
+ clustering, and anomaly detection.
99
+ """
100
+
101
+ def __init__(self, patch_size=16, num_bands=8, num_timestamps=10,
102
+ spatial_embedding_dim=128, temporal_embedding_dim=128):
103
+ self.patch_size = patch_size
104
+ self.num_bands = num_bands
105
+ self.num_timestamps = num_timestamps
106
+ self.spatial_embedding_dim = spatial_embedding_dim
107
+ self.temporal_embedding_dim = temporal_embedding_dim
108
+
109
+ # Build encoders
110
+ self.spatial_encoder = SpatialEncoder(embedding_dim=spatial_embedding_dim)
111
+ self.temporal_encoder = TemporalEncoder(
112
+ embedding_dim=temporal_embedding_dim,
113
+ lstm_units=64
114
+ )
115
+
116
+ # Build spatial encoder input
117
+ self.spatial_encoder.build((None, patch_size, patch_size, num_bands))
118
+
119
+ # Clustering and anomaly detection (fitted during inference)
120
+ self.kmeans = None
121
+ self.anomaly_detector = None
122
+ self.scaler = StandardScaler()
123
+
124
+ def encode_spatial_features(self, patches: np.ndarray) -> np.ndarray:
125
+ """
126
+ Extract spatial features from all patches and timestamps.
127
+
128
+ Args:
129
+ patches: Array of shape (num_patches, time, height, width, bands)
130
+
131
+ Returns:
132
+ spatial_embeddings: Array of shape (num_patches, time, spatial_embedding_dim)
133
+ """
134
+ num_patches, time, height, width, bands = patches.shape
135
+
136
+ # Reshape to process all patches and timestamps together
137
+ # (num_patches * time, height, width, bands)
138
+ reshaped = patches.reshape(-1, height, width, bands)
139
+
140
+ # Extract spatial features
141
+ spatial_features = self.spatial_encoder(reshaped, training=False).numpy()
142
+
143
+ # Reshape back to (num_patches, time, embedding_dim)
144
+ spatial_embeddings = spatial_features.reshape(
145
+ num_patches, time, self.spatial_embedding_dim
146
+ )
147
+
148
+ return spatial_embeddings
149
+
150
+ def encode_temporal_features(self, spatial_embeddings: np.ndarray) -> np.ndarray:
151
+ """
152
+ Extract temporal features from spatial embeddings.
153
+
154
+ Args:
155
+ spatial_embeddings: Array of shape (num_patches, time, spatial_embedding_dim)
156
+
157
+ Returns:
158
+ temporal_embeddings: Array of shape (num_patches, temporal_embedding_dim)
159
+ """
160
+ temporal_embeddings = self.temporal_encoder(
161
+ spatial_embeddings, training=False
162
+ ).numpy()
163
+
164
+ return temporal_embeddings
165
+
166
+ def cluster_stress_patterns(self, embeddings: np.ndarray, n_clusters=4) -> Tuple[np.ndarray, np.ndarray]:
167
+ """
168
+ Cluster embeddings into stress categories and compute stress scores.
169
+
170
+ Args:
171
+ embeddings: Array of shape (num_patches, embedding_dim)
172
+ n_clusters: Number of clusters (4: high, moderate, low, noise)
173
+
174
+ Returns:
175
+ cluster_labels: Cluster assignment for each patch
176
+ stress_scores: Normalized stress scores in [0, 1]
177
+ """
178
+ # Standardize embeddings
179
+ embeddings_scaled = self.scaler.fit_transform(embeddings)
180
+
181
+ # K-Means clustering
182
+ self.kmeans = KMeans(n_clusters=n_clusters, random_state=42, n_init=10)
183
+ cluster_labels = self.kmeans.fit_predict(embeddings_scaled)
184
+
185
+ # Compute stress scores based on distance to cluster centers
186
+ distances = self.kmeans.transform(embeddings_scaled)
187
+
188
+ # For each patch, compute stress score as weighted distance to all clusters
189
+ # Normalize to [0, 1] range
190
+ stress_scores = np.min(distances, axis=1) # Distance to nearest cluster
191
+ stress_scores = 1 - (stress_scores - stress_scores.min()) / (stress_scores.max() - stress_scores.min() + 1e-10)
192
+
193
+ # Alternative: Use cluster centers to assign stress levels
194
+ # Identify which cluster represents highest stress (largest distance from origin)
195
+ cluster_stress_levels = np.linalg.norm(self.kmeans.cluster_centers_, axis=1)
196
+ cluster_stress_levels = (cluster_stress_levels - cluster_stress_levels.min()) / \
197
+ (cluster_stress_levels.max() - cluster_stress_levels.min() + 1e-10)
198
+
199
+ # Assign stress score based on cluster membership
200
+ stress_scores = cluster_stress_levels[cluster_labels]
201
+
202
+ return cluster_labels, stress_scores
203
+
204
+ def detect_anomalies(self, embeddings: np.ndarray, contamination=0.1) -> Tuple[np.ndarray, np.ndarray]:
205
+ """
206
+ Detect anomalous stress patterns using Isolation Forest.
207
+
208
+ Args:
209
+ embeddings: Array of shape (num_patches, embedding_dim)
210
+ contamination: Expected proportion of anomalies
211
+
212
+ Returns:
213
+ anomaly_labels: 1 for normal, -1 for anomaly
214
+ anomaly_scores: Anomaly scores (lower = more anomalous)
215
+ """
216
+ self.anomaly_detector = IsolationForest(
217
+ contamination=contamination,
218
+ random_state=42
219
+ )
220
+ anomaly_labels = self.anomaly_detector.fit_predict(embeddings)
221
+ anomaly_scores = self.anomaly_detector.score_samples(embeddings)
222
+
223
+ return anomaly_labels, anomaly_scores
224
+
225
+ def predict(self, patches: np.ndarray, n_clusters=4, contamination=0.1) -> Dict:
226
+ """
227
+ Complete stress detection pipeline.
228
+
229
+ Args:
230
+ patches: Array of shape (num_patches, time, height, width, bands)
231
+ n_clusters: Number of stress clusters
232
+ contamination: Expected proportion of anomalies
233
+
234
+ Returns:
235
+ results: Dictionary with all predictions and embeddings
236
+ """
237
+ # Step 1: Spatial encoding
238
+ spatial_embeddings = self.encode_spatial_features(patches)
239
+
240
+ # Step 2: Temporal encoding
241
+ temporal_embeddings = self.encode_temporal_features(spatial_embeddings)
242
+
243
+ # Step 3: Clustering
244
+ cluster_labels, stress_scores = self.cluster_stress_patterns(
245
+ temporal_embeddings, n_clusters=n_clusters
246
+ )
247
+
248
+ # Step 4: Anomaly detection
249
+ anomaly_labels, anomaly_scores = self.detect_anomalies(temporal_embeddings, contamination=contamination)
250
+
251
+ return {
252
+ 'spatial_embeddings': spatial_embeddings,
253
+ 'temporal_embeddings': temporal_embeddings,
254
+ 'cluster_labels': cluster_labels,
255
+ 'stress_scores': stress_scores,
256
+ 'anomaly_labels': anomaly_labels,
257
+ 'anomaly_scores': anomaly_scores,
258
+ 'cluster_centers': self.kmeans.cluster_centers_,
259
+ 'n_clusters': n_clusters
260
+ }
261
+
262
+
263
+ def get_stress_category(stress_score: float) -> str:
264
+ """Convert stress score to category label."""
265
+ if stress_score < 0.25:
266
+ return "Low Stress"
267
+ elif stress_score < 0.5:
268
+ return "Moderate Stress"
269
+ elif stress_score < 0.75:
270
+ return "High Stress"
271
+ else:
272
+ return "Severe Stress"
273
+
274
+
275
+ def find_optimal_clusters(embeddings: np.ndarray,
276
+ min_clusters: int = 2,
277
+ max_clusters: int = 10) -> Tuple[int, Dict]:
278
+ """
279
+ Find optimal number of clusters using elbow method and silhouette score.
280
+
281
+ Args:
282
+ embeddings: Array of shape (num_samples, embedding_dim)
283
+ min_clusters: Minimum number of clusters to test
284
+ max_clusters: Maximum number of clusters to test
285
+
286
+ Returns:
287
+ optimal_k: Optimal number of clusters
288
+ metrics: Dictionary with inertia and silhouette scores
289
+ """
290
+ print("\nFinding optimal number of clusters...")
291
+
292
+ scaler = StandardScaler()
293
+ embeddings_scaled = scaler.fit_transform(embeddings)
294
+
295
+ inertias = []
296
+ silhouette_scores = []
297
+ k_range = range(min_clusters, max_clusters + 1)
298
+
299
+ for k in k_range:
300
+ kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
301
+ labels = kmeans.fit_predict(embeddings_scaled)
302
+
303
+ inertias.append(kmeans.inertia_)
304
+
305
+ # Calculate silhouette score (higher is better)
306
+ if k > 1:
307
+ sil_score = silhouette_score(embeddings_scaled, labels)
308
+ silhouette_scores.append(sil_score)
309
+ else:
310
+ silhouette_scores.append(0)
311
+
312
+ print(f" k={k}: Inertia={kmeans.inertia_:.2f}, Silhouette={silhouette_scores[-1]:.3f}")
313
+
314
+ # Find elbow using rate of change
315
+ inertia_diffs = np.diff(inertias)
316
+ inertia_diffs_2 = np.diff(inertia_diffs)
317
+
318
+ # Optimal k is where second derivative is maximum (elbow point)
319
+ elbow_k = min_clusters + np.argmax(np.abs(inertia_diffs_2)) + 1
320
+
321
+ # Also consider silhouette score
322
+ best_silhouette_k = min_clusters + np.argmax(silhouette_scores)
323
+
324
+ # Use silhouette score as primary metric, elbow as secondary
325
+ optimal_k = best_silhouette_k
326
+
327
+ print(f"\n[OK] Optimal clusters: {optimal_k} (Elbow: {elbow_k}, Best Silhouette: {best_silhouette_k})")
328
+
329
+ metrics = {
330
+ 'k_range': list(k_range),
331
+ 'inertias': inertias,
332
+ 'silhouette_scores': silhouette_scores,
333
+ 'optimal_k': optimal_k,
334
+ 'elbow_k': elbow_k,
335
+ 'best_silhouette_k': best_silhouette_k
336
+ }
337
+
338
+ return optimal_k, metrics
339
+
340
+
341
+ def prepare_llm_context(results: Dict,
342
+ patch_coords: List,
343
+ patches: np.ndarray,
344
+ metadata: Dict) -> Dict:
345
+ """
346
+ Prepare comprehensive context for LLM including cluster statistics and anomaly information.
347
+
348
+ Args:
349
+ results: Dictionary from model.predict()
350
+ patch_coords: List of (h, w) coordinates for each patch
351
+ patches: Original patches array
352
+ metadata: Preprocessing metadata
353
+
354
+ Returns:
355
+ context: Dictionary with cluster-wise and anomaly statistics
356
+ """
357
+ cluster_labels = results['cluster_labels']
358
+ stress_scores = results['stress_scores']
359
+ anomaly_labels = results['anomaly_labels']
360
+ temporal_embeddings = results['temporal_embeddings']
361
+
362
+ # Get anomaly scores (distance from decision boundary)
363
+ anomaly_scores = results.get('anomaly_scores',
364
+ results['anomaly_labels'].astype(float))
365
+
366
+ # Cluster-wise statistics
367
+ cluster_stats = []
368
+ for cluster_id in range(results['n_clusters']):
369
+ cluster_mask = cluster_labels == cluster_id
370
+ cluster_patches = patches[cluster_mask]
371
+ cluster_stress = stress_scores[cluster_mask]
372
+ cluster_embeddings = temporal_embeddings[cluster_mask]
373
+
374
+ # Calculate statistics for this cluster
375
+ stats = {
376
+ 'cluster_id': int(cluster_id),
377
+ 'num_patches': int(np.sum(cluster_mask)),
378
+ 'percentage': float(100 * np.sum(cluster_mask) / len(cluster_labels)),
379
+ 'stress_score': {
380
+ 'mean': float(cluster_stress.mean()),
381
+ 'std': float(cluster_stress.std()),
382
+ 'min': float(cluster_stress.min()),
383
+ 'max': float(cluster_stress.max())
384
+ },
385
+ 'embedding_stats': {
386
+ 'mean_norm': float(np.linalg.norm(cluster_embeddings.mean(axis=0))),
387
+ 'std_norm': float(np.linalg.norm(cluster_embeddings.std(axis=0)))
388
+ },
389
+ 'band_statistics': {}
390
+ }
391
+
392
+ # Calculate per-band statistics for this cluster
393
+ for band_idx, band_name in enumerate(metadata['selected_bands']):
394
+ band_data = cluster_patches[:, :, :, :, band_idx] # (patches, time, h, w)
395
+ stats['band_statistics'][band_name] = {
396
+ 'mean': float(np.nanmean(band_data)),
397
+ 'std': float(np.nanstd(band_data)),
398
+ 'min': float(np.nanmin(band_data)),
399
+ 'max': float(np.nanmax(band_data))
400
+ }
401
+
402
+ cluster_stats.append(stats)
403
+
404
+ # Calculate temporal trends for this cluster
405
+ # Shape: (num_patches, time, h, w, bands) -> (time, bands)
406
+ if cluster_patches.shape[0] > 0:
407
+ cluster_time_series = np.nanmean(cluster_patches, axis=(0, 2, 3))
408
+
409
+ stats['temporal_trends'] = {}
410
+ for band_idx, band_name in enumerate(metadata['selected_bands']):
411
+ series = cluster_time_series[:, band_idx]
412
+ if len(series) > 1:
413
+ change = float(series[-1] - series[0])
414
+ trend_direction = "stable"
415
+ if change > 0.05: trend_direction = "increasing"
416
+ elif change < -0.05: trend_direction = "decreasing"
417
+
418
+ stats['temporal_trends'][band_name] = {
419
+ 'change': change,
420
+ 'trend_direction': trend_direction,
421
+ 'latest_value': float(series[-1]),
422
+ 'earliest_value': float(series[0])
423
+ }
424
+
425
+ # Anomaly information
426
+ anomaly_mask = anomaly_labels == -1
427
+ anomaly_indices = np.where(anomaly_mask)[0]
428
+
429
+ anomaly_info = {
430
+ 'total_anomalies': int(np.sum(anomaly_mask)),
431
+ 'anomaly_percentage': float(100 * np.sum(anomaly_mask) / len(anomaly_labels)),
432
+ 'anomaly_patches': []
433
+ }
434
+
435
+ # Detailed info for each anomaly patch
436
+ for idx in anomaly_indices[:20]: # Limit to first 20 anomalies
437
+ patch_info = {
438
+ 'patch_id': int(idx),
439
+ 'coordinates': patch_coords[idx],
440
+ 'stress_score': float(stress_scores[idx]),
441
+ 'stress_category': get_stress_category(stress_scores[idx]),
442
+ 'cluster_id': int(cluster_labels[idx]),
443
+ 'anomaly_score': float(anomaly_scores[idx]) if hasattr(anomaly_scores, '__getitem__') else -1.0,
444
+ 'embedding_norm': float(np.linalg.norm(temporal_embeddings[idx]))
445
+ }
446
+ anomaly_info['anomaly_patches'].append(patch_info)
447
+
448
+ # Overall field statistics
449
+ field_stats = {
450
+ 'total_patches': len(cluster_labels),
451
+ 'patch_size': metadata['patch_size'],
452
+ 'num_bands': metadata['num_bands'],
453
+ 'selected_bands': metadata['selected_bands'],
454
+ 'overall_stress': {
455
+ 'mean': float(stress_scores.mean()),
456
+ 'std': float(stress_scores.std()),
457
+ 'min': float(stress_scores.min()),
458
+ 'max': float(stress_scores.max())
459
+ },
460
+ 'stress_distribution': {
461
+ 'low': int(np.sum(stress_scores < 0.25)),
462
+ 'moderate': int(np.sum((stress_scores >= 0.25) & (stress_scores < 0.5))),
463
+ 'high': int(np.sum((stress_scores >= 0.5) & (stress_scores < 0.75))),
464
+ 'severe': int(np.sum(stress_scores >= 0.75))
465
+ }
466
+ }
467
+
468
+ context = {
469
+ 'field_statistics': field_stats,
470
+ 'cluster_statistics': cluster_stats,
471
+ 'anomaly_information': anomaly_info
472
+ }
473
+
474
+ return context
475
+
stress_detection_preprocessing.py ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Stress Detection Preprocessing Module
3
+ ======================================
4
+
5
+ Prepares Sentinel-2 multi-spectral data for stress detection model.
6
+ Includes band harmonization and normalization.
7
+ """
8
+
9
+ import numpy as np
10
+ from typing import Tuple, List
11
+
12
+ # Band indices in the 12-band Sentinel-2 data
13
+ BAND_INDICES = {
14
+ 'B02': 1, # Blue
15
+ 'B03': 2, # Green
16
+ 'B04': 3, # Red
17
+ 'B05': 4, # Red Edge 1
18
+ 'B08': 7, # NIR
19
+ 'B8A': 8, # NIR Narrow
20
+ 'B11': 10, # SWIR1
21
+ 'B12': 11 # SWIR2
22
+ }
23
+
24
+ # Selected bands for stress detection (8 major bands)
25
+ SELECTED_BANDS = ['B02', 'B03', 'B04', 'B05', 'B08', 'B8A', 'B11', 'B12']
26
+
27
+
28
+ def extract_major_bands(all_images: np.ndarray) -> np.ndarray:
29
+ """
30
+ Extract 8 major bands from 12-band Sentinel-2 data.
31
+
32
+ Args:
33
+ all_images: Array of shape (time, height, width, 12)
34
+
35
+ Returns:
36
+ Array of shape (time, height, width, 8) with selected bands
37
+ """
38
+ band_idx = [BAND_INDICES[band] for band in SELECTED_BANDS]
39
+ return all_images[:, :, :, band_idx]
40
+
41
+
42
+ def harmonize_bands(images: np.ndarray) -> np.ndarray:
43
+ """
44
+ Harmonize band data to same scale [0, 1].
45
+
46
+ Sentinel-2 reflectance values are already in [0, 1] range after DN/10000 conversion.
47
+ This function ensures all bands are properly normalized and handles any outliers.
48
+
49
+ Args:
50
+ images: Array of shape (time, height, width, bands)
51
+
52
+ Returns:
53
+ Harmonized array with values clipped to [0, 1]
54
+ """
55
+ # Clip to [0, 1] range to handle any outliers
56
+ harmonized = np.clip(images, 0, 1)
57
+
58
+ # Additional per-band normalization to ensure uniform scale
59
+ # Use percentile-based normalization to handle outliers
60
+ time, height, width, bands = harmonized.shape
61
+
62
+ for b in range(bands):
63
+ band_data = harmonized[:, :, :, b]
64
+
65
+ # Calculate 2nd and 98th percentiles to handle outliers
66
+ p2 = np.nanpercentile(band_data, 2)
67
+ p98 = np.nanpercentile(band_data, 98)
68
+
69
+ # Normalize to [0, 1] using percentiles
70
+ if p98 > p2:
71
+ harmonized[:, :, :, b] = np.clip((band_data - p2) / (p98 - p2), 0, 1)
72
+
73
+ return harmonized
74
+
75
+
76
+ def handle_nan_values(images: np.ndarray, method='mean') -> np.ndarray:
77
+ """
78
+ Handle NaN values in the data.
79
+
80
+ Args:
81
+ images: Array of shape (time, height, width, bands)
82
+ method: 'mean', 'zero', or 'interpolate'
83
+
84
+ Returns:
85
+ Array with NaN values handled
86
+ """
87
+ if method == 'zero':
88
+ return np.nan_to_num(images, nan=0.0)
89
+ elif method == 'mean':
90
+ # Replace NaN with temporal mean for each pixel
91
+ return np.where(np.isnan(images),
92
+ np.nanmean(images, axis=0, keepdims=True),
93
+ images)
94
+ elif method == 'interpolate':
95
+ # Simple linear interpolation along time axis
96
+ result = images.copy()
97
+ time, height, width, bands = images.shape
98
+
99
+ for h in range(height):
100
+ for w in range(width):
101
+ for b in range(bands):
102
+ pixel_series = result[:, h, w, b]
103
+ if np.any(np.isnan(pixel_series)):
104
+ # Interpolate NaN values
105
+ mask = ~np.isnan(pixel_series)
106
+ if np.any(mask):
107
+ indices = np.arange(time)
108
+ result[:, h, w, b] = np.interp(
109
+ indices, indices[mask], pixel_series[mask]
110
+ )
111
+ else:
112
+ result[:, h, w, b] = 0.0
113
+ return result
114
+ else:
115
+ return images
116
+
117
+
118
+ def create_patches(images: np.ndarray, patch_size: int = 16, stride: int = 8) -> Tuple[np.ndarray, List]:
119
+ """
120
+ Create overlapping patches from images for spatial analysis.
121
+
122
+ Args:
123
+ images: Array of shape (time, height, width, bands)
124
+ patch_size: Size of each patch
125
+ stride: Stride for patch extraction
126
+
127
+ Returns:
128
+ patches: Array of shape (num_patches, time, patch_size, patch_size, bands)
129
+ patch_coords: List of (h_start, w_start) coordinates for each patch
130
+ """
131
+ time, height, width, bands = images.shape
132
+ patches = []
133
+ patch_coords = []
134
+
135
+ for h in range(0, height - patch_size + 1, stride):
136
+ for w in range(0, width - patch_size + 1, stride):
137
+ patch = images[:, h:h+patch_size, w:w+patch_size, :]
138
+
139
+ # Only include patches with sufficient valid data
140
+ valid_ratio = np.sum(~np.isnan(patch)) / patch.size
141
+ if valid_ratio > 0.5: # At least 50% valid data
142
+ patches.append(patch)
143
+ patch_coords.append((h, w))
144
+
145
+ if len(patches) == 0:
146
+ # If no valid patches, create at least one from center
147
+ h_center = (height - patch_size) // 2
148
+ w_center = (width - patch_size) // 2
149
+ patch = images[:, h_center:h_center+patch_size, w_center:w_center+patch_size, :]
150
+ patches.append(patch)
151
+ patch_coords.append((h_center, w_center))
152
+
153
+ return np.array(patches), patch_coords
154
+
155
+
156
+ def preprocess_for_model(all_images: np.ndarray,
157
+ patch_size: int = 16,
158
+ stride: int = 8) -> Tuple[np.ndarray, List, dict]:
159
+ """
160
+ Complete preprocessing pipeline for stress detection model.
161
+
162
+ Args:
163
+ all_images: Raw images of shape (time, height, width, 12)
164
+ patch_size: Size of patches for spatial analysis
165
+ stride: Stride for patch extraction
166
+
167
+ Returns:
168
+ patches: Preprocessed patches ready for model
169
+ patch_coords: Coordinates of each patch
170
+ metadata: Dictionary with preprocessing information
171
+ """
172
+ print("Preprocessing data for stress detection model...")
173
+
174
+ # Step 1: Extract major bands
175
+ print(" [1/4] Extracting 8 major bands...")
176
+ major_bands = extract_major_bands(all_images)
177
+
178
+ # Step 2: Harmonize bands to same scale
179
+ print(" [2/4] Harmonizing bands to [0, 1] scale...")
180
+ harmonized = harmonize_bands(major_bands)
181
+
182
+ # Step 3: Handle NaN values
183
+ print(" [3/4] Handling NaN values...")
184
+ clean_data = handle_nan_values(harmonized, method='mean')
185
+
186
+ # Step 4: Create patches
187
+ print(" [4/4] Creating spatial patches...")
188
+ patches, patch_coords = create_patches(clean_data, patch_size, stride)
189
+
190
+ metadata = {
191
+ 'original_shape': all_images.shape,
192
+ 'selected_bands': SELECTED_BANDS,
193
+ 'num_bands': len(SELECTED_BANDS),
194
+ 'patch_size': patch_size,
195
+ 'stride': stride,
196
+ 'num_patches': len(patches),
197
+ 'harmonized': True
198
+ }
199
+
200
+ print(f"[OK] Preprocessing complete. Created {len(patches)} patches.")
201
+ print(f" Patch shape: {patches.shape}")
202
+
203
+ return patches, patch_coords, metadata
vegetation_indices.py ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Vegetation Indices Calculator
3
+ ==============================
4
+
5
+ This module provides functions to calculate 10 vegetation indices from Sentinel-2 data
6
+ and perform temporal analysis.
7
+
8
+ Indices:
9
+ - NDVI: Normalized Difference Vegetation Index
10
+ - EVI: Enhanced Vegetation Index
11
+ - NDWI: Normalized Difference Water Index
12
+ - NDRE: Normalized Difference Red Edge
13
+ - RECI: Red Edge Chlorophyll Index
14
+ - SMI: Soil Moisture Index
15
+ - NDSI: Normalized Difference Snow Index
16
+ - PRI: Photochemical Reflectance Index
17
+ - PSRI: Plant Senescence Reflectance Index
18
+ - MCARI: Modified Chlorophyll Absorption Ratio Index
19
+ - SASI: Salinity Index
20
+ - SOMI: Soil Organic Matter Index
21
+ - SFI: Soil Fertility Index
22
+ """
23
+
24
+ import numpy as np
25
+ from typing import Dict, List, Tuple
26
+ import datetime
27
+ from datetime import timedelta
28
+
29
+ # ==================== INDEX CALCULATION FUNCTIONS ====================
30
+
31
+ def calculate_ndvi(img: np.ndarray) -> np.ndarray:
32
+ """NDVI = (NIR - RED) / (NIR + RED)"""
33
+ nir = img[:, :, 7] # B08
34
+ red = img[:, :, 3] # B04
35
+ return (nir - red) / (nir + red + 1e-10)
36
+
37
+ def calculate_evi(img: np.ndarray) -> np.ndarray:
38
+ """EVI = 2.5 * ((NIR - RED) / (NIR + 6*RED - 7.5*BLUE + 1))"""
39
+ nir = img[:, :, 7] # B08
40
+ red = img[:, :, 3] # B04
41
+ blue = img[:, :, 1] # B02
42
+ return 2.5 * ((nir - red) / (nir + 6*red - 7.5*blue + 1))
43
+
44
+ def calculate_ndwi(img: np.ndarray) -> np.ndarray:
45
+ """NDWI = (GREEN - NIR) / (GREEN + NIR)"""
46
+ green = img[:, :, 2] # B03
47
+ nir = img[:, :, 7] # B08
48
+ return (green - nir) / (green + nir + 1e-10)
49
+
50
+ def calculate_ndre(img: np.ndarray) -> np.ndarray:
51
+ """NDRE = (NIR - RedEdge) / (NIR + RedEdge)"""
52
+ nir = img[:, :, 7] # B08
53
+ red_edge = img[:, :, 4] # B05
54
+ return (nir - red_edge) / (nir + red_edge + 1e-10)
55
+
56
+ def calculate_reci(img: np.ndarray) -> np.ndarray:
57
+ """RECI = (NIR / RedEdge) - 1"""
58
+ nir = img[:, :, 7] # B08
59
+ red_edge = img[:, :, 4] # B05
60
+ return (nir / (red_edge + 1e-10)) - 1
61
+
62
+ def calculate_smi(img: np.ndarray) -> np.ndarray:
63
+ """SMI = (SWIR1 - SWIR2) / (SWIR1 + SWIR2)"""
64
+ swir1 = img[:, :, 10] # B11
65
+ swir2 = img[:, :, 11] # B12
66
+ return (swir1 - swir2) / (swir1 + swir2 + 1e-10)
67
+
68
+ def calculate_ndsi(img: np.ndarray) -> np.ndarray:
69
+ """NDSI = (GREEN - SWIR1) / (GREEN + SWIR1)"""
70
+ green = img[:, :, 2] # B03
71
+ swir1 = img[:, :, 10] # B11
72
+ return (green - swir1) / (green + swir1 + 1e-10)
73
+
74
+ def calculate_pri(img: np.ndarray) -> np.ndarray:
75
+ """PRI = (B02 - B03) / (B02 + B03)"""
76
+ b02 = img[:, :, 1] # B02
77
+ b03 = img[:, :, 2] # B03
78
+ return (b02 - b03) / (b02 + b03 + 1e-10)
79
+
80
+ def calculate_psri(img: np.ndarray) -> np.ndarray:
81
+ """PSRI = (RED - GREEN) / NIR"""
82
+ red = img[:, :, 3] # B04
83
+ green = img[:, :, 2] # B03
84
+ nir = img[:, :, 7] # B08
85
+ return (red - green) / (nir + 1e-10)
86
+
87
+ def calculate_mcari(img: np.ndarray) -> np.ndarray:
88
+ """MCARI = ((B05 - B04) - 0.2 * (B05 - B03)) * (B05 / B04)"""
89
+ b03 = img[:, :, 2] # B03
90
+ b04 = img[:, :, 3] # B04
91
+ b05 = img[:, :, 4] # B05
92
+ return ((b05 - b04) - 0.2 * (b05 - b03)) * (b05 / (b04 + 1e-10))
93
+
94
+ def calculate_sasi(img: np.ndarray) -> np.ndarray:
95
+ """SASI (Salinity Index) = SQRT(B11 * B04)"""
96
+ swir1 = img[:, :, 10] # B11
97
+ red = img[:, :, 3] # B04
98
+ return np.sqrt(swir1 * red)
99
+
100
+ def calculate_somi(img: np.ndarray) -> np.ndarray:
101
+ """SOMI (Soil Organic Matter Index) = (B08 + B04) / (B11 + B12)"""
102
+ nir = img[:, :, 7] # B08
103
+ red = img[:, :, 3] # B04
104
+ swir1 = img[:, :, 10] # B11
105
+ swir2 = img[:, :, 11] # B12
106
+ return (nir + red) / (swir1 + swir2 + 1e-10)
107
+
108
+ def calculate_sfi(img: np.ndarray) -> np.ndarray:
109
+ """SFI (Soil Fertility Index) = (NDVI * SOMI) / SASI
110
+ Combines vegetation health, organic matter, and salinity"""
111
+ ndvi = calculate_ndvi(img)
112
+ somi = calculate_somi(img)
113
+ sasi = calculate_sasi(img)
114
+ return (ndvi * somi) / (sasi + 1e-10)
115
+
116
+ # Index registry
117
+ INDEX_FUNCTIONS = {
118
+ 'NDVI': calculate_ndvi,
119
+ 'EVI': calculate_evi,
120
+ 'NDWI': calculate_ndwi,
121
+ 'NDRE': calculate_ndre,
122
+ 'RECI': calculate_reci,
123
+ 'SMI': calculate_smi,
124
+ 'NDSI': calculate_ndsi,
125
+ 'PRI': calculate_pri,
126
+ 'PSRI': calculate_psri,
127
+ 'MCARI': calculate_mcari,
128
+ 'SASI': calculate_sasi,
129
+ 'SOMI': calculate_somi,
130
+ 'SFI': calculate_sfi
131
+ }
132
+
133
+ # ==================== BATCH CALCULATION ====================
134
+
135
+ def calculate_all_indices(img: np.ndarray) -> Dict[str, np.ndarray]:
136
+ """
137
+ Calculate all 10 vegetation indices for a single image.
138
+
139
+ Args:
140
+ img: Image array of shape (height, width, 12) with reflectance values
141
+
142
+ Returns:
143
+ Dictionary mapping index names to 2D arrays
144
+ """
145
+ indices = {}
146
+ for name, func in INDEX_FUNCTIONS.items():
147
+ indices[name] = func(img)
148
+ return indices
149
+
150
+ def calculate_indices_temporal(images: np.ndarray) -> Dict[str, np.ndarray]:
151
+ """
152
+ Calculate all indices for multiple time steps.
153
+
154
+ Args:
155
+ images: Array of shape (time, height, width, 12)
156
+
157
+ Returns:
158
+ Dictionary mapping index names to 3D arrays (time, height, width)
159
+ """
160
+ indices_data = {}
161
+
162
+ for index_name, calc_func in INDEX_FUNCTIONS.items():
163
+ index_series = []
164
+ for img in images:
165
+ index_map = calc_func(img)
166
+ index_series.append(index_map)
167
+ indices_data[index_name] = np.array(index_series)
168
+
169
+ return indices_data
170
+
171
+ # ==================== STATISTICS ====================
172
+
173
+ def get_field_statistics(index_map: np.ndarray) -> Dict[str, float]:
174
+ """
175
+ Calculate statistics for a single index map.
176
+
177
+ Returns:
178
+ Dictionary with mean, std, min, max, median
179
+ """
180
+ return {
181
+ 'mean': float(np.nanmean(index_map)),
182
+ 'std': float(np.nanstd(index_map)),
183
+ 'min': float(np.nanmin(index_map)),
184
+ 'max': float(np.nanmax(index_map)),
185
+ 'median': float(np.nanmedian(index_map)),
186
+ 'valid_pixels': int(np.sum(~np.isnan(index_map)))
187
+ }
188
+
189
+ def get_temporal_statistics(indices_temporal: Dict[str, np.ndarray]) -> Dict[str, Dict]:
190
+ """
191
+ Calculate temporal statistics for all indices.
192
+
193
+ Args:
194
+ indices_temporal: Dictionary with index names and 3D arrays (time, height, width)
195
+
196
+ Returns:
197
+ Dictionary with temporal stats for each index
198
+ """
199
+ temporal_stats = {}
200
+
201
+ for index_name, data in indices_temporal.items():
202
+ stats = {
203
+ 'mean_over_time': np.nanmean(data, axis=0),
204
+ 'std_over_time': np.nanstd(data, axis=0),
205
+ 'max_over_time': np.nanmax(data, axis=0),
206
+ 'min_over_time': np.nanmin(data, axis=0),
207
+ 'range': np.nanmax(data, axis=0) - np.nanmin(data, axis=0),
208
+ 'temporal_trend': data[-1] - data[0] if len(data) >= 2 else np.zeros_like(data[0]),
209
+ }
210
+
211
+ # Rolling average (window size = 3)
212
+ if len(data) >= 3:
213
+ rolling_avg = np.array([np.nanmean(data[max(0, i-2):i+1], axis=0)
214
+ for i in range(len(data))])
215
+ stats['rolling_avg_3'] = rolling_avg
216
+
217
+ temporal_stats[index_name] = stats
218
+
219
+ return temporal_stats
220
+
221
+ def get_summary_report(indices_temporal: Dict[str, np.ndarray],
222
+ dates: List[str]) -> Dict:
223
+ """
224
+ Generate a comprehensive summary report.
225
+
226
+ Returns:
227
+ Dictionary with summary statistics and temporal trends
228
+ """
229
+ report = {
230
+ 'dates': dates,
231
+ 'num_images': len(dates),
232
+ 'indices': {}
233
+ }
234
+
235
+ for index_name, data in indices_temporal.items():
236
+ index_report = {
237
+ 'latest': get_field_statistics(data[-1]),
238
+ 'oldest': get_field_statistics(data[0]),
239
+ 'mean_values_over_time': [float(np.nanmean(data[i])) for i in range(len(dates))],
240
+ 'change': float(np.nanmean(data[-1]) - np.nanmean(data[0])),
241
+ 'max_in_field': float(np.nanmax(data)),
242
+ 'min_in_field': float(np.nanmin(data))
243
+ }
244
+ report['indices'][index_name] = index_report
245
+
246
+ return report