| import os |
| import xarray as xr |
| import numpy as np |
| import pandas as pd |
| import geopandas as gpd |
| from shapely.geometry import Point, Polygon |
| import copernicusmarine |
| from typing import Dict, List, Tuple, Optional |
| import json |
| import folium |
| from datetime import datetime, timedelta |
| import warnings |
| from scipy.spatial import cKDTree |
|
|
| from grid_system import ExplorationGridSystem |
|
|
|
|
| class GlobalWindFarmPlanner(ExplorationGridSystem): |
| """ |
| Performance-optimized wind farm planner with vectorized operations and data chunking. |
| """ |
| |
| def __init__(self, region: str = "africa", cell_size_km: float = 10.0, |
| max_grid_cells: int = 5000, custom_bounds: Dict = None): |
| """ |
| Initialize optimized global wind farm planner. |
| |
| Args: |
| region: Target region |
| cell_size_km: Grid cell size in kilometers |
| max_grid_cells: Maximum grid cells to prevent memory issues |
| custom_bounds: Custom bounds (optional) |
| """ |
| self.region = region |
| self.max_grid_cells = max_grid_cells |
|
|
| if custom_bounds: |
| self.bounds = custom_bounds |
| else: |
| self.bounds = self._get_region_bounds(region) |
| |
| |
| estimated_cells = self._estimate_grid_size(cell_size_km) |
| if estimated_cells > max_grid_cells: |
| adjusted_size = self._calculate_optimal_cell_size() |
| print(f"Adjusting cell size from {cell_size_km}km to {adjusted_size:.1f}km for performance") |
| cell_size_km = adjusted_size |
| |
| super().__init__(cell_size_km=cell_size_km, study_bounds=self.bounds) |
| |
| |
| |
| |
| |
|
|
| copernicus_username = os.getenv('COPERNICUS_USERNAME') |
| copernicus_password = os.getenv('COPERNICUS_PASSWORD') |
| if copernicus_username and copernicus_password: |
| copernicusmarine.login( |
| username=copernicus_username, |
| password=copernicus_password, |
| force_overwrite=True |
| ) |
| print("Copernicus Marine login successful") |
| else: |
| print("Copernicus credentials not found in environment variables") |
| self.data_availability['using_fallback'] = True |
|
|
| |
| self.wind_data = None |
| self.wave_data = None |
| self.data_availability = { |
| 'wind_current': False, |
| 'wave_historical': False, |
| 'using_fallback': False |
| } |
| |
| def _estimate_grid_size(self, cell_size_km: float) -> int: |
| """Estimate number of grid cells for given cell size.""" |
| lat_range = self.bounds['max_lat'] - self.bounds['min_lat'] |
| lon_range = self.bounds['max_lon'] - self.bounds['min_lon'] |
| cell_size_degrees = cell_size_km / 111.0 |
| |
| n_lat = int(lat_range / cell_size_degrees) |
| n_lon = int(lon_range / cell_size_degrees) |
| |
| |
| return int(n_lat * n_lon * 0.3) |
| |
| def _calculate_optimal_cell_size(self) -> float: |
| """Calculate optimal cell size to stay under max_grid_cells limit.""" |
| lat_range = self.bounds['max_lat'] - self.bounds['min_lat'] |
| lon_range = self.bounds['max_lon'] - self.bounds['min_lon'] |
| |
| |
| target_cells = self.max_grid_cells / 0.3 |
| target_dimension = int(np.sqrt(target_cells)) |
| |
| optimal_lat_size = lat_range / target_dimension |
| optimal_lon_size = lon_range / target_dimension |
| optimal_size_degrees = max(optimal_lat_size, optimal_lon_size) |
| |
| return optimal_size_degrees * 111.0 |
| |
| def _get_region_bounds(self, region: str) -> Dict: |
| """Get bounding box for different regions with performance-optimized sizes.""" |
| |
| region_bounds = { |
| "africa": { |
| 'min_lat': -35.0, 'max_lat': 37.0, |
| 'min_lon': -25.0, 'max_lon': 52.0 |
| }, |
| "europe": { |
| 'min_lat': 50.0, 'max_lat': 72.0, |
| 'min_lon': -15.0, 'max_lon': 35.0 |
| }, |
| "asia": { |
| 'min_lat': 20.0, 'max_lat': 45.0, |
| 'min_lon': 100.0, 'max_lon': 150.0 |
| }, |
| "north_america_east": { |
| 'min_lat': 30.0, 'max_lat': 50.0, |
| 'min_lon': -85.0, 'max_lon': -60.0 |
| }, |
| "north_america_west": { |
| 'min_lat': 30.0, 'max_lat': 50.0, |
| 'min_lon': -130.0, 'max_lon': -115.0 |
| } |
| } |
| |
| return region_bounds.get(region, region_bounds["africa"]) |
| |
| def load_copernicus_wind_data_optimized(self, months_back: int = 3) -> Optional[xr.Dataset]: |
| """ |
| Load wind data with optimized spatial/temporal chunking. |
| Reduced months_back for faster loading. |
| """ |
| print(f"Loading optimized wind data for {self.region} ({months_back} months)...") |
| |
| try: |
| end_date = datetime.now() - timedelta(days=30) |
| start_date = end_date - timedelta(days=months_back * 30) |
| |
| |
| self.wind_data = copernicusmarine.open_dataset( |
| dataset_id='cmems_obs-wind_glo_phy_my_l4_0.125deg_PT1H', |
| minimum_longitude=self.bounds['min_lon'], |
| maximum_longitude=self.bounds['max_lon'], |
| minimum_latitude=self.bounds['min_lat'], |
| maximum_latitude=self.bounds['max_lat'], |
| start_datetime=start_date.strftime('%Y-%m-%d'), |
| end_datetime=end_date.strftime('%Y-%m-%d') |
| ) |
| |
| |
| print("Downsampling to daily averages for performance...") |
| self.wind_data = self.wind_data.resample(time='1D').mean() |
| |
| self.data_availability['wind_current'] = True |
| print(f"Wind data loaded and downsampled: {self.wind_data.dims}") |
| return self.wind_data |
| |
| except Exception as e: |
| print(f"Wind data unavailable: {e}") |
| print("Using optimized synthetic data...") |
| self.data_availability['using_fallback'] = True |
| return self._create_optimized_synthetic_wind_data() |
| |
| def _create_optimized_synthetic_wind_data(self) -> xr.Dataset: |
| """Create lightweight synthetic wind data.""" |
| |
| |
| lat_step = max(0.25, (self.bounds['max_lat'] - self.bounds['min_lat']) / 50) |
| lon_step = max(0.25, (self.bounds['max_lon'] - self.bounds['min_lon']) / 50) |
| |
| lat_range = np.arange(self.bounds['min_lat'], self.bounds['max_lat'], lat_step) |
| lon_range = np.arange(self.bounds['min_lon'], self.bounds['max_lon'], lon_step) |
| |
| |
| time_range = pd.date_range('2023-01-01', periods=30, freq='D') |
| |
| print(f"Creating synthetic wind data: {len(lat_range)} x {len(lon_range)} x {len(time_range)}") |
| |
| |
| lat_grid, lon_grid = np.meshgrid(lat_range, lon_range, indexing='ij') |
| |
| |
| if self.region == "africa": |
| base_u = -3 + 2*np.sin(np.radians(lon_grid)) + np.cos(np.radians(lat_grid)) |
| base_v = 2 + np.sin(np.radians(lat_grid)) |
| elif self.region == "europe": |
| base_u = 8 + 2*np.cos(np.radians(lat_grid)) |
| base_v = 1 + np.sin(np.radians(lon_grid)) |
| else: |
| base_u = 6 + np.sin(np.radians(lat_grid + lon_grid)) |
| base_v = 3 + np.cos(np.radians(lat_grid)) |
| |
| |
| eastward_wind = np.broadcast_to(base_u[np.newaxis, :, :], (len(time_range), len(lat_range), len(lon_range))) |
| northward_wind = np.broadcast_to(base_v[np.newaxis, :, :], (len(time_range), len(lat_range), len(lon_range))) |
| |
| |
| np.random.seed(42) |
| eastward_wind = eastward_wind + np.random.normal(0, 1, eastward_wind.shape) |
| northward_wind = northward_wind + np.random.normal(0, 0.8, northward_wind.shape) |
| |
| return xr.Dataset({ |
| 'eastward_wind': (('time', 'latitude', 'longitude'), eastward_wind), |
| 'northward_wind': (('time', 'latitude', 'longitude'), northward_wind) |
| }, coords={ |
| 'time': time_range, |
| 'latitude': lat_range, |
| 'longitude': lon_range |
| }) |
| |
| def load_copernicus_wave_data_optimized(self, months_back: int = 3) -> Optional[xr.Dataset]: |
| """ |
| Load wave data from Copernicus Marine Service. |
| """ |
| print(f"Loading wave data for {self.region} ({months_back} months)...") |
| |
| try: |
| |
| |
|
|
| |
| end_date = datetime(2020, 12, 31) |
| start_date = end_date - timedelta(days=months_back * 30) |
| |
| |
| self.wave_data = copernicusmarine.open_dataset( |
| dataset_id='cmems_obs-wave_glo_phy-swh_my_multi-l4-0.5deg_P1D-i', |
| minimum_longitude=self.bounds['min_lon'], |
| maximum_longitude=self.bounds['max_lon'], |
| minimum_latitude=self.bounds['min_lat'], |
| maximum_latitude=self.bounds['max_lat'], |
| start_datetime=start_date.strftime('%Y-%m-%d'), |
| end_datetime=end_date.strftime('%Y-%m-%d') |
| ) |
| |
| |
| print("Downsampling wave data to daily averages...") |
| self.wave_data = self.wave_data.resample(time='1D').mean() |
| |
| self.data_availability['wave_historical'] = True |
| print(f"Wave data loaded: {self.wave_data.dims}") |
| return self.wave_data |
| |
| except Exception as e: |
| print(f"Wave data unavailable: {e}") |
| print("Using synthetic wave data...") |
| self.data_availability['using_fallback'] = True |
| return self._create_synthetic_wave_data() |
|
|
| def _create_synthetic_wave_data(self) -> xr.Dataset: |
| """Create synthetic wave data as fallback.""" |
| lat_step = max(0.25, (self.bounds['max_lat'] - self.bounds['min_lat']) / 50) |
| lon_step = max(0.25, (self.bounds['max_lon'] - self.bounds['min_lon']) / 50) |
| |
| lat_range = np.arange(self.bounds['min_lat'], self.bounds['max_lat'], lat_step) |
| lon_range = np.arange(self.bounds['min_lon'], self.bounds['max_lon'], lon_step) |
| time_range = pd.date_range('2023-01-01', periods=30, freq='D') |
| |
| lat_grid, lon_grid = np.meshgrid(lat_range, lon_range, indexing='ij') |
| |
| |
| if self.region == "africa": |
| |
| base_wave_height = 2.0 + np.where(lon_grid < 10, 1.0, 0.5) + abs(lat_grid) * 0.02 |
| elif self.region == "europe": |
| |
| base_wave_height = 1.5 + abs(lat_grid - 55) * 0.05 |
| else: |
| base_wave_height = 2.0 + abs(lat_grid) * 0.03 |
| |
| |
| wave_height = np.broadcast_to(base_wave_height[np.newaxis, :, :], |
| (len(time_range), len(lat_range), len(lon_range))) |
| |
| |
| np.random.seed(42) |
| wave_height = wave_height + np.random.normal(0, 0.3, wave_height.shape) |
| wave_height = np.maximum(wave_height, 0.5) |
| |
| return xr.Dataset({ |
| 'VHM0': (('time', 'latitude', 'longitude'), wave_height) |
| }, coords={ |
| 'time': time_range, |
| 'latitude': lat_range, |
| 'longitude': lon_range |
| }) |
|
|
|
|
| def calculate_wind_resource_score_vectorized(self) -> np.ndarray: |
| """Vectorized wind resource calculation using spatial interpolation.""" |
| print("Calculating wind resource scores (vectorized)...") |
| |
| if self.wind_data is None: |
| self.load_copernicus_wind_data_optimized() |
| |
| |
| wind_speed = np.sqrt( |
| self.wind_data['eastward_wind']**2 + self.wind_data['northward_wind']**2 |
| ) |
| |
| wind_mean = wind_speed.mean(dim='time') |
| wind_capacity = xr.where( |
| (wind_speed >= 3) & (wind_speed <= 25), |
| xr.where(wind_speed <= 12, (wind_speed / 12) ** 3, 1.0), |
| 0.0 |
| ).mean(dim='time') |
| |
| |
| grid_lats = self.grid_gdf['center_lat'].values |
| grid_lons = self.grid_gdf['center_lon'].values |
| |
| |
| wind_mean_interp = wind_mean.interp( |
| latitude=xr.DataArray(grid_lats, dims='points'), |
| longitude=xr.DataArray(grid_lons, dims='points'), |
| method='linear' |
| ).values |
| |
| wind_capacity_interp = wind_capacity.interp( |
| latitude=xr.DataArray(grid_lats, dims='points'), |
| longitude=xr.DataArray(grid_lons, dims='points'), |
| method='linear' |
| ).values |
| |
| |
| scores = np.where( |
| wind_mean_interp > 25, 0.1, |
| np.where( |
| wind_mean_interp < 3, 0.0, |
| wind_capacity_interp * 0.8 + (wind_mean_interp / 15) * 0.2 |
| ) |
| ) |
| |
| |
| scores = np.nan_to_num(scores, nan=0.0) |
| |
| self.scores['wind_resource'] = scores |
| print(f"Wind resource scoring complete (vectorized). Best score: {scores.max():.3f}") |
| return scores |
| |
| def calculate_wave_operational_score_fast(self) -> np.ndarray: |
| """Calculate wave scores using VAVH_INST from Copernicus data.""" |
| print("Calculating wave operational scores (using Copernicus data)...") |
| |
| if self.wave_data is None: |
| self.load_copernicus_wave_data_optimized() |
| |
| |
| swh = self.wave_data["VAVH_INST"] |
| flag = self.wave_data["VAVH_INST_FLAG"] |
| swh_good = swh.where(flag == 1) |
| |
| wave_height_mean = swh_good.mean(dim='time') |
| |
| grid_lats = self.grid_gdf['center_lat'].values |
| grid_lons = self.grid_gdf['center_lon'].values |
| |
| wave_mean_interp = wave_height_mean.interp( |
| latitude=xr.DataArray(grid_lats, dims='points'), |
| longitude=xr.DataArray(grid_lons, dims='points'), |
| method='linear' |
| ).values |
| |
| scores = np.where( |
| wave_mean_interp > 4.0, 0.1, |
| np.where( |
| wave_mean_interp < 1.0, 0.8, |
| 1.0 - (wave_mean_interp - 1.0) / 3.0 * 0.7 |
| ) |
| ) |
| |
| scores = np.nan_to_num(scores, nan=0.5) |
| |
| self.scores['wave_operational'] = scores |
| print(f"Wave operational scoring complete. Best score: {scores.max():.3f}") |
| return scores |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| def _vectorized_coast_distance(self, grid_coords: np.ndarray) -> np.ndarray: |
| """Vectorized distance to coast calculation.""" |
| |
| |
| if self.region == "africa": |
| coast_points = np.array([ |
| [-10, 0], [-15, 10], [-10, 20], [0, 35], |
| [15, 35], [25, 30], [35, 20], [40, 0], |
| [45, -10], [35, -25], [20, -35], [0, -30] |
| ]) |
| elif self.region == "europe": |
| coast_points = np.array([ |
| [-10, 50], [-5, 55], [0, 60], [10, 65], |
| [15, 60], [25, 55], [30, 65] |
| ]) |
| else: |
| |
| coast_points = np.array([ |
| [self.bounds['min_lon'], self.bounds['min_lat']], |
| [self.bounds['max_lon'], self.bounds['min_lat']], |
| [self.bounds['max_lon'], self.bounds['max_lat']], |
| [self.bounds['min_lon'], self.bounds['max_lat']] |
| ]) |
| |
| |
| coast_tree = cKDTree(coast_points) |
| |
| |
| distances, _ = coast_tree.query(grid_coords[:, [1, 0]]) |
| |
| |
| return distances * 111 |
| |
| def calculate_distance_to_shore_score_fast(self) -> np.ndarray: |
| """Fast distance scoring using pre-calculated coast distances.""" |
| print("Calculating distance to shore scores (fast)...") |
| |
| grid_coords = np.column_stack([ |
| self.grid_gdf['center_lat'].values, |
| self.grid_gdf['center_lon'].values |
| ]) |
| |
| shore_distances = self._vectorized_coast_distance(grid_coords) |
| |
| |
| scores = np.where( |
| (shore_distances >= 10) & (shore_distances <= 30), 1.0, |
| np.where( |
| (shore_distances >= 5) & (shore_distances < 10), 0.7, |
| np.where( |
| (shore_distances > 30) & (shore_distances <= 100), |
| 1.0 - (shore_distances - 30) / 70 * 0.5, |
| np.where(shore_distances > 100, 0.2, 0.1) |
| ) |
| ) |
| ) |
| |
| self.scores['distance_to_shore'] = scores |
| print(f"Distance to shore scoring complete (fast). Best score: {scores.max():.3f}") |
| return scores |
| |
| def calculate_environmental_sensitivity_score_for_windfarm(self) -> np.ndarray: |
| """ |
| Calculate environmental sensitivity for wind farm development. |
| Higher score = higher environmental sensitivity = worse for development. |
| """ |
| print("Calculating environmental sensitivity scores for wind farms...") |
| |
| scores = np.zeros(len(self.grid_gdf)) |
| |
| for idx, cell in self.grid_gdf.iterrows(): |
| lat = cell['center_lat'] |
| lon = cell['center_lon'] |
| |
| |
| sensitivity = 0.3 |
| |
| |
| |
| if self.region == "africa": |
| |
| west_coast_dist = abs(lon - (-10)) |
| east_coast_dist = abs(lon - 40) |
| coastal_dist = min(west_coast_dist, east_coast_dist) |
| elif self.region == "europe": |
| |
| coastal_dist = abs(lon - 0) |
| else: |
| coastal_dist = 5 |
| |
| |
| coastal_sensitivity = max(0, 0.4 - (coastal_dist * 0.05)) |
| |
| |
| if self.region == "africa": |
| if -5 <= lat <= 5: |
| ecosystem_sensitivity = 0.4 |
| elif lat < -20 or lat > 20: |
| ecosystem_sensitivity = 0.2 |
| else: |
| ecosystem_sensitivity = 0.3 |
| elif self.region == "europe": |
| if 54 <= lat <= 62: |
| ecosystem_sensitivity = 0.3 |
| else: |
| ecosystem_sensitivity = 0.2 |
| else: |
| ecosystem_sensitivity = 0.25 |
| |
| |
| estimated_depth = coastal_dist * 15 |
| if estimated_depth < 10: |
| depth_sensitivity = 0.3 |
| elif estimated_depth > 100: |
| depth_sensitivity = 0.2 |
| else: |
| depth_sensitivity = 0.1 |
| |
| |
| total_sensitivity = ( |
| sensitivity + |
| coastal_sensitivity + |
| ecosystem_sensitivity + |
| depth_sensitivity |
| ) |
| |
| scores[idx] = min(1.0, total_sensitivity) |
| |
| print(f"Environmental sensitivity scoring complete. Average: {scores.mean():.3f}") |
| return scores |
|
|
|
|
|
|
| def create_wind_farm_map(top_sites: gpd.GeoDataFrame, |
| all_sites: gpd.GeoDataFrame, |
| region: str, |
| weights: Dict[str, float], |
| max_background_points: int = 1000) -> str: |
| """Create optimized map with limited background points for speed.""" |
| |
| |
| if len(all_sites) > 0: |
| |
| bounds = all_sites.bounds |
| center_lat = (bounds.miny.min() + bounds.maxy.max()) / 2 |
| center_lon = (bounds.minx.min() + bounds.maxx.max()) / 2 |
| |
| |
| lat_range = bounds.maxy.max() - bounds.miny.min() |
| lon_range = bounds.maxx.max() - bounds.minx.min() |
| max_range = max(lat_range, lon_range) |
| |
| |
| if max_range > 40: |
| zoom_level = 3 |
| elif max_range > 20: |
| zoom_level = 4 |
| elif max_range > 10: |
| zoom_level = 5 |
| elif max_range > 5: |
| zoom_level = 6 |
| else: |
| zoom_level = 7 |
| |
| else: |
| |
| center_lat = (top_sites['center_lat'].min() + top_sites['center_lat'].max()) / 2 |
| center_lon = (top_sites['center_lon'].min() + top_sites['center_lon'].max()) / 2 |
| zoom_level = 5 |
| |
| m = folium.Map( |
| location=[center_lat, center_lon], |
| zoom_start=zoom_level, |
| tiles="CartoDB positron" |
| ) |
| |
| |
| sample_size = min(max_background_points, len(all_sites)) |
| if sample_size < len(all_sites): |
| sampled_sites = all_sites.sample(n=sample_size, random_state=42) |
| else: |
| sampled_sites = all_sites |
| |
| |
| for idx, site in sampled_sites.iterrows(): |
| suitability = site.get('suitability_score', 0) |
| |
| color = ('darkgreen' if suitability > 0.7 else |
| 'green' if suitability > 0.5 else |
| 'orange' if suitability > 0.3 else 'red') |
| |
| folium.CircleMarker( |
| location=[site['center_lat'], site['center_lon']], |
| radius=2, |
| color=color, |
| fill=True, |
| fillOpacity=0.6, |
| weight=0 |
| ).add_to(m) |
| |
| |
| for idx, site in top_sites.iterrows(): |
| folium.Marker( |
| location=[site['center_lat'], site['center_lon']], |
| popup=f""" |
| <b>Wind Farm Site #{idx+1}</b><br> |
| Suitability: {site['suitability_score']:.3f}<br> |
| Wind: {site['wind_resource_score']:.3f}<br> |
| Waves: {site['wave_operational_score']:.3f} |
| """, |
| icon=folium.Icon(color='green', icon='leaf') |
| ).add_to(m) |
| |
| |
| legend_html = f''' |
| <div style="position: fixed; top: 10px; right: 10px; width: 200px; height: 120px; |
| background-color: white; border:1px solid grey; z-index:9999; |
| font-size:11px; padding: 8px;"> |
| <h4 style="margin:0;">Wind Farm Sites - {region.title()}</h4> |
| <p style="margin:2px 0;">🟢 Excellent (>0.7)</p> |
| <p style="margin:2px 0;">🟡 Good (0.5-0.7)</p> |
| <p style="margin:2px 0;">🟠 Fair (0.3-0.5)</p> |
| <p style="margin:2px 0;">🔴 Poor (<0.3)</p> |
| <p style="margin:2px 0;">🍃 Top {len(top_sites)} sites</p> |
| </div> |
| ''' |
| m.get_root().html.add_child(folium.Element(legend_html)) |
| |
| return m.get_root().render() |
|
|
|
|
| def generate_wind_farm_report(top_sites: gpd.GeoDataFrame, |
| weights: Dict[str, float], |
| region: str, |
| goal: str, |
| data_availability: Dict[str, bool], |
| min_wind_resource: float, |
| max_wave_height: float, |
| fast_mode: bool, |
| total_time: float) -> str: |
| """Generate optimized report with performance metrics.""" |
| |
| report_lines = [] |
| |
| report_lines.append(f"# OPTIMIZED WIND FARM PLANNING REPORT - {region.upper()}") |
| report_lines.append("=" * 60) |
| report_lines.append(f"**Analysis completed in {total_time:.1f} seconds**") |
| report_lines.append(f"**Region:** {region.title()}") |
| report_lines.append(f"**Mode:** {'Fast' if fast_mode else 'Standard'}") |
| report_lines.append("") |
| |
| |
| report_lines.append("## PERFORMANCE SUMMARY") |
| report_lines.append(f"- **Total Processing Time:** {total_time:.1f} seconds") |
| report_lines.append(f"- **Grid Cells Analyzed:** {len(top_sites)}") |
| report_lines.append(f"- **Analysis Mode:** {'Fast algorithms' if fast_mode else 'Standard algorithms'}") |
| if data_availability['using_fallback']: |
| report_lines.append("- **Data Source:** Synthetic (Copernicus unavailable)") |
| else: |
| report_lines.append("- **Data Source:** Copernicus Marine Service") |
| report_lines.append("") |
| |
| |
| report_lines.append("## TOP WIND FARM SITES") |
| report_lines.append("| Rank | Coordinates | Suitability | Wind | Operations |") |
| report_lines.append("|------|-------------|-------------|------|------------|") |
| |
| for idx, site in top_sites.iterrows(): |
| coords = f"{site['center_lat']:.1f}°N, {abs(site['center_lon']):.1f}°{'W' if site['center_lon'] < 0 else 'E'}" |
| report_lines.append( |
| f"| {idx+1} | {coords} | {site['suitability_score']:.3f} | " |
| f"{site['wind_resource_score']:.3f} | {site['wave_operational_score']:.3f} |" |
| ) |
| |
| report_lines.append("") |
| |
| |
| avg_suitability = top_sites['suitability_score'].mean() |
| |
| report_lines.append("## DEVELOPMENT ASSESSMENT") |
| if avg_suitability > 0.7: |
| report_lines.append("**HIGH POTENTIAL** - Excellent conditions identified") |
| elif avg_suitability > 0.5: |
| report_lines.append("**MODERATE POTENTIAL** - Good development opportunities") |
| else: |
| report_lines.append("**LIMITED POTENTIAL** - Consider alternative approaches") |
| |
| report_lines.append("") |
| report_lines.append("## NEXT STEPS") |
| report_lines.append("1. Detailed resource assessment at top 3 sites") |
| report_lines.append("2. Environmental impact evaluation") |
| report_lines.append("3. Grid connection feasibility study") |
| |
| if fast_mode or data_availability['using_fallback']: |
| report_lines.append("") |
| report_lines.append("**Note:** This was a rapid assessment. Consider detailed analysis with:") |
| report_lines.append("- Current oceanographic measurements") |
| report_lines.append("- Higher resolution spatial analysis") |
| report_lines.append("- Extended temporal datasets") |
| |
| return "\n".join(report_lines) |
|
|
|
|
|
|
|
|