# 🌞 Raydium - Solar Potential Analysis for India

Raydium aims to map India's solar potential. This program is still heavily WIP and contributions would be really appreciated. 

## 🌟 Features

- **Solar Potential Mapping**: Fetches and calculates solar potential from NASA POWER API data 🌞
- **Interactive Visualization**: Displays solar potential over India with interactive maps 🖼️
- **High-Resolution Interpolation**: Uses grid sampling for a detailed solar potential map 🗺️
- **Automated with GitHub Actions**: Automatically runs daily or on-demand in GitHub Actions 🕒

## 🛠️ Setup

### Prerequisites
- Python 3.8+
- Required packages in `raydium.py` include: `geopandas`, `folium`, `pvlib`, `requests`, `beautifulsoup4`

### 1️⃣ Clone the Repository

```bash
git clone https://github.com/your-username/raydium.git
cd raydium
```

### 2️⃣ Install Dependencies

```bash
pip install -r requirements.txt
```

Or manually install dependencies with:

```bash
pip install geopandas folium pvlib requests beautifulsoup4
```

### 3️⃣ Run the Script Locally

1. **Upload GeoJSON File**: Ensure you have the `india-soi.geojson` file in the root directory.
2. **Run the Python Script**:

   ```bash
   python raydium.py
   ```

   - This will generate `india_solar_potential.html` (an interactive map) and `india_solar_data.csv` (raw data) in your project directory.

## 🌐 GitHub Actions Workflow

To automate the solar analysis daily or on-demand, Raydium uses GitHub Actions! 🎉

1. **Enable GitHub Actions**:
   - The workflow is defined in `.github/workflows/run_raydium.yml`.
2. **Triggers**:
   - Run daily at midnight UTC (modifiable by editing the `cron`).
   - Or trigger manually from the **Actions** tab.

#### 📝 Workflow Overview

```yaml
name: Generate Solar Potential Data

on:
  schedule:
    - cron: '0 0 * * *'  # Run daily at midnight (00:00)
  workflow_dispatch:  # Allow manual trigger

jobs:
  generate-solar-data:
    runs-on: ubuntu-latest
    
    steps:
    - name: Checkout repository
      uses: actions/checkout@v3
      
    - name: Set up Python
      uses: actions/setup-python@v4
      with:
        python-version: '3.10'
        
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install numpy pandas geopandas folium requests scipy branca
        
    - name: Run solar potential analysis
      run: |
        python raydium.py
      env:
        PYTHONUNBUFFERED: 1
        
    - name: Check if files were generated
      run: |
        if [ ! -f india_solar_potential.html ] || [ ! -f india_solar_data.csv ]; then
          echo "Error: Required files were not generated"
          exit 1
        fi
        
    - name: Configure Git
      run: |
        git config --local user.email "github-actions[bot]@users.noreply.github.com"
        git config --local user.name "github-actions[bot]"
        
    - name: Commit and push if there are changes
      run: |
        git add india_solar_potential.html india_solar_data.csv
        git diff --staged --quiet || (git commit -m "Update solar potential data [skip ci]" && git push)
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

```

## 📊 Output

- **india_solar_potential.html**: Interactive map showcasing solar potential across India.
- **india_solar_data.csv**: Raw solar potential data for each grid point sampled.

Both files are accessible as artifacts in GitHub Actions and are downloadable for further analysis.

## 📂 File Structure

```plaintext
.
├── raydium.py                  # Main script for fetching and visualizing solar potential
├── india-soi.geojson           # GeoJSON file of India's boundaries
├── .github
│   └── workflows
│       └── run_raydium.yml     # GitHub Actions workflow
├── README.md                   # Project documentation
└── requirements.txt            # Dependencies for local setup
```

## 📋 Credits

Special thanks to [DataMeet](https://github.com/datameet/maps/blob/master/Country/india-soi.geojson) for the GeoJSON boundary file of India and to [NASA POWER API](https://power.larc.nasa.gov/docs/services/api/) for the data required to calculate the solar potential.

Can you edit the above so that it better fits the program below:
import numpy as np
import pandas as pd
import geopandas as gpd
import folium
import requests
from shapely.geometry import Point
from scipy.interpolate import griddata
import time
import branca.colormap as cm
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
import os
from concurrent.futures import ThreadPoolExecutor, as_completed
import hashlib
import json
import shelve
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry
import logging
import threading
from tqdm import tqdm

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(message)s',
    handlers=[
        logging.FileHandler("solar_map.log"),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)

class RateLimiter:
    def __init__(self, max_calls, period):
        self.max_calls = max_calls
        self.period = period
        self.lock = threading.Lock()
        self.calls = []
    
    def acquire(self):
        with self.lock:
            current = time.time()
            while self.calls and self.calls[0] <= current - self.period:
                self.calls.pop(0)
            if len(self.calls) >= self.max_calls:
                sleep_time = self.period - (current - self.calls[0]) + 0.1
                logger.info(f"Rate limit reached. Sleeping for {sleep_time:.2f} seconds.")
                time.sleep(sleep_time)
            self.calls.append(time.time())

rate_limiter = RateLimiter(max_calls=1, period=2)

def create_session_with_retry():
    session = requests.Session()
    retries = Retry(
        total=10,
        backoff_factor=2,
        status_forcelist=[429, 500, 502, 503, 504],
        allowed_methods=["HEAD", "GET", "OPTIONS"],
        respect_retry_after_header=True
    )
    adapter = HTTPAdapter(max_retries=retries)
    session.mount('https://', adapter)
    session.mount('http://', adapter)
    return session

def fetch_nasa_power_data(lat, lon, cache, session):
    key = f"{lat}_{lon}"
    if key in cache:
        return cache[key]
    
    base_url = "https://power.larc.nasa.gov/api/temporal/daily/point"
    params = {
        'parameters': 'ALLSKY_SFC_SW_DWN',
        'community': 'RE',
        'longitude': str(lon),
        'latitude': str(lat),
        'start': '20230101',
        'end': '20231231',
        'format': 'JSON'
    }
    
    while True:
        try:
            rate_limiter.acquire()
            response = session.get(base_url, params=params)
            if response.status_code == 429:
                retry_after = response.headers.get('Retry-After')
                if retry_after:
                    sleep_time = int(retry_after) + 1
                else:
                    sleep_time = 60
                logger.warning(f"Received 429 for lat={lat}, lon={lon}. Retrying after {sleep_time} seconds.")
                time.sleep(sleep_time)
                continue
            response.raise_for_status()
            data = response.json()
            cache[key] = data
            return data
        except requests.exceptions.RequestException as e:
            logger.error(f"RequestException for lat={lat}, lon={lon}: {e}. Retrying in 10 seconds.")
            time.sleep(10)
        except json.JSONDecodeError as e:
            logger.error(f"JSONDecodeError for lat={lat}, lon={lon}: {e}. Skipping this point.")
            cache[key] = None
            return None

def calculate_solar_potential(power_data, panel_efficiency=0.2):
    if not power_data:
        return None
    try:
        daily_data = power_data['properties']['parameter']['ALLSKY_SFC_SW_DWN']
        radiation_values = [v * 0.277778 for v in daily_data.values() 
                            if isinstance(v, (int, float)) and v != -999]
        if not radiation_values:
            return None
        return np.mean(radiation_values) * 365 * panel_efficiency
    except KeyError as e:
        logger.error(f"KeyError while calculating solar potential: {e}")
        return None
    except Exception as e:
        logger.error(f"Unexpected error while calculating solar potential: {e}")
        return None

def fetch_and_calculate(lat, lon, cache, session):
    power_data = fetch_nasa_power_data(lat, lon, cache, session)
    potential = calculate_solar_potential(power_data)
    if potential is not None:
        return {'latitude': lat, 'longitude': lon, 'potential': potential}
    return None

def create_india_solar_map(geojson_path='india-soi.geojson'):
    logger.info(f"Reading GeoJSON file from: {geojson_path}")
    india = gpd.read_file(geojson_path)
    india_simplified = india.geometry.simplify(0.1)
    india_union = india_simplified.unary_union
    bounds = india_union.bounds
    logger.info(f"Map bounds: {bounds}")
    m = folium.Map(location=[20.5937, 78.9629], zoom_start=5, tiles='CartoDB positron')
    lat_step = 0.25
    lon_step = 0.25
    lat_range = np.arange(bounds[1], bounds[3] + lat_step, lat_step)
    lon_range = np.arange(bounds[0], bounds[2] + lon_step, lon_step)
    cache_file = 'nasa_power_cache.db'
    cache = shelve.open(cache_file)
    session = create_session_with_retry()
    max_workers = 3
    logger.info(f"Starting data collection with {max_workers} workers...")
    solar_data = []
    grid_points = [(lat, lon) for lat in lat_range for lon in lon_range if india_union.contains(Point(lon, lat)) or india_union.touches(Point(lon, lat))]
    total_grid_points = len(grid_points)
    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        futures = {executor.submit(fetch_and_calculate, lat, lon, cache, session): (lat, lon) for lat, lon in grid_points}
        for future in tqdm(as_completed(futures), total=total_grid_points, desc="Processing Points"):
            result = future.result()
            if result:
                solar_data.append(result)
    cache.close()
    solar_df = pd.DataFrame(solar_data)
    if solar_df.empty:
        raise ValueError("No solar data collected")
    logger.info("Creating interpolation grid...")
    grid_size = 800
    grid_lat = np.linspace(bounds[1], bounds[3], grid_size)
    grid_lon = np.linspace(bounds[0], bounds[2], grid_size)
    grid_lon, grid_lat = np.meshgrid(grid_lon, grid_lat)
    points = solar_df[['longitude', 'latitude']].values
    values = solar_df['potential'].values
    grid_z = griddata(points, values, (grid_lon, grid_lat), method='linear')
    logger.info("Creating mask for India's boundary...")
    grid_points_mesh = [Point(lon, lat) for lon, lat in zip(grid_lon.flatten(), grid_lat.flatten())]
    india_gdf = gpd.GeoSeries(grid_points_mesh)
    mask = india_gdf.within(india_union).values.reshape(grid_z.shape)
    grid_z = np.ma.masked_array(grid_z, ~mask)
    logger.info("Creating visualization...")
    colormap = cm.LinearColormap(
        ['#440154', '#482878', '#3E4989', '#31688E', '#26828E',
         '#1F9E89', '#35B779', '#6DCD59', '#B4DE2C', '#FDE725'],
        vmin=np.nanmin(values),
        vmax=np.nanmax(values),
        caption='Solar Potential (kWh/m²/year)'
    )
    norm = plt.Normalize(vmin=colormap.vmin, vmax=colormap.vmax)
    cmap = ListedColormap(colormap.colors)
    fig, ax = plt.subplots(figsize=(16, 12))
    ax.set_axis_off()
    ax.imshow(
        grid_z, 
        cmap=cmap, 
        extent=(bounds[0], bounds[2], bounds[1], bounds[3]),
        origin='lower', 
        aspect='auto'
    )
    image_path = 'solar_potential_high_res.png'
    plt.savefig(image_path, bbox_inches='tight', pad_inches=0, dpi=600)
    plt.close(fig)
    if not os.path.exists(image_path):
        raise FileNotFoundError(f"Failed to create image at {image_path}")
    folium.raster_layers.ImageOverlay(
        name='Solar Potential',
        image=image_path,
        bounds=[[bounds[1], bounds[0]], [bounds[3], bounds[2]]],
        opacity=0.6,
        interactive=True,
        cross_origin=False,
        zindex=1,
    ).add_to(m)
    folium.GeoJson(
        india,
        style_function=lambda x: {
            'fillColor': 'none',
            'color': 'black',
            'weight': 1.5,
            'fillOpacity': 0
        },
        name='India Boundary'
    ).add_to(m)
    colormap.add_to(m)
    folium.LayerControl().add_to(m)
    output_csv = 'india_solar_data.csv'
    output_html = 'india_solar_potential.html'
    logger.info(f"Saving data to {output_csv}")
    solar_df.to_csv(output_csv, index=False)
    logger.info(f"Saving map to {output_html}")
    m.save(output_html)
    logger.info("Solar potential map generation completed successfully.")
    return m, solar_df

def main():
    try:
        logger.info("Starting solar potential map generation with adaptive rate limiting...")
        solar_map, solar_data = create_india_solar_map()
        logger.info("Process completed successfully!")
        logger.info("Map has been saved as 'india_solar_potential.html'")
        logger.info("Raw data has been saved as 'india_solar_data.csv'")
    except Exception as e:
        logger.error(f"An error occurred: {e}")

main()