Source code for RES.cell


import warnings

import geopandas as gpd
import numpy as np
from shapely.geometry import box

from RES import utility as utils
from RES.AttributesParser import AttributesParser
from RES.boundaries import GADMBoundaries
from RES.era5_cutout import ERA5Cutout
from RES.hdf5_handler import DataHandler

[docs] class GridCells(AttributesParser): """ Spatial grid cell generator for renewable energy resource assessment. This class creates a regular spatial grid covering a specified region for discretized renewable energy potential analysis. It inherits from AttributesParser for configuration management and integrates with ERA5Cutout and GADMBoundaries to maintain consistency with climate data spatial resolution and regional boundaries. Grid cells serve as the fundamental spatial units for capacity calculations, land availability analysis, and resource aggregation. Each cell represents a homogeneous area with uniform resource characteristics and constraints. Parameters ---------- config_file_path : str or Path Path to configuration file containing grid settings region_short_code : str Region identifier for boundary definition resource_type : str Resource type ('solar' or 'wind') Attributes ---------- ERA5Cutout : ERA5Cutout ERA5 climate data cutout handler instance gadmBoundary : GADMBoundaries GADM boundary processor instance datahandler : DataHandler HDF5 data storage interface for grid persistence crs : str Coordinate reference system ('EPSG:4326') resolution : dict Grid resolution with 'dx' and 'dy' keys (decimal degrees) bounding_box : dict Spatial extent with 'minx', 'maxx', 'miny', 'maxy' actual_boundary : gpd.GeoDataFrame Precise regional boundary geometry coords : dict Grid coordinate arrays {'x': array, 'y': array} shape : tuple Grid dimensions (rows, columns) bounding_box_grid : gpd.GeoDataFrame Complete grid covering bounding box region grid_cells : gpd.GeoDataFrame Final grid cells intersecting with regional boundary (custom grid) cutout : atlite.Cutout ERA5 cutout object with climate data region_boundary : gpd.GeoDataFrame Regional boundary from ERA5 processing resource_grid_cells : gpd.GeoDataFrame Grid cells from default ERA5-based processing Methods ------- generate_coords() -> None Create coordinate arrays based on resolution and boundary __get_grid__() -> gpd.GeoDataFrame Generate complete grid with cell geometries (private method) get_custom_grid() -> gpd.GeoDataFrame Create custom grid cells intersecting with regional boundary get_default_grid() -> gpd.GeoDataFrame Create grid using ERA5 cutout methodology with climate data alignment _check_resolution() -> None Validate resolution settings and issue warnings (private method, not currently used) Examples -------- Generate grid for British Columbia wind assessment: >>> from RES.cell import GridCells >>> grid = GridCells( ... config_file_path="config/config_BC.yaml", ... region_short_code="BC", ... resource_type="wind" ... ) >>> # Using custom grid approach >>> custom_cells = grid.get_custom_grid() >>> print(f"Generated {len(custom_cells)} custom grid cells") >>> >>> # Using default ERA5-aligned grid >>> default_cells = grid.get_default_grid() >>> print(f"Generated {len(default_cells)} ERA5-aligned grid cells") Custom resolution configuration: >>> # In configuration file (config_BC.yaml): >>> # grid_cell_resolution: >>> # dx: 0.25 # 0.25 degrees longitude >>> # dy: 0.25 # 0.25 degrees latitude >>> grid._check_resolution() # Validate resolution settings Notes ----- - Default resolution matches ERA5 climate data (0.25° x 0.25°) - Grid cells are represented as square polygons with centroid coordinates - Inherits configuration management from AttributesParser - Integrates with ERA5Cutout for climate data alignment - Uses GADMBoundaries for precise regional boundary definition - Uses HDF5 storage for efficient caching of large grid datasets - Grid generation respects regional boundaries to avoid unnecessary cells - Resolution warnings issued if finer than climate data resolution - Coordinate system maintained as WGS84 for global compatibility - Supports both custom grid generation and ERA5-aligned grid generation Grid Generation Approaches -------------------------- 1. Custom Grid (get_custom_grid()): - Creates grid based on regional bounding box - Intersects with precise regional boundaries - Stores results in HDF5 with 'cells' key 2. Default Grid (get_default_grid()): - Uses ERA5 cutout grid as base - Aligns with climate data resolution - Overlays with regional boundaries - Stores both 'cells' and 'boundary' in HDF5 Resolution Considerations ------------------------- - Minimum recommended: 0.25° (matching ERA5 resolution) - Harmonized resolutions required for interpolation of climate data - Coarser resolutions may miss local variations in resource quality - Square cells assumed (dx = dy) for geometric consistency - Resolution validation available via _check_resolution() method Dependencies ------------ - geopandas: Spatial data manipulation - numpy: Numerical operations for coordinate generation - shapely.geometry.box: Grid cell geometry creation - RES.AttributesParser: Parent class for configuration management - RES.boundaries.GADMBoundaries: Regional boundary processing - RES.era5_cutout.ERA5Cutout: Climate data cutout handling - RES.hdf5_handler.DataHandler: HDF5 data storage interface - RES.utility: Utility functions for cell ID assignment and logging """ def __post_init__(self): """ Initializes the bounding box and resolution after the parent class initialization. Accepts a resolution dictionary to define the x and y resolutions. """ # Call the parent class __post_init__ to initialize inherited attributes super().__post_init__() # This dictionary will be used to pass arguments to external classes self.required_args = { #order doesn't matter "config_file_path": self.config_file_path, "region_short_code": self.region_short_code, "resource_type": self.resource_type } self.ERA5Cutout=ERA5Cutout(**self.required_args) self.gadmBoundary=GADMBoundaries(**self.required_args) ## Initiate the Store and Datahandler (interfacing with the Store) self.datahandler=DataHandler(self.store) self.crs=self.get_default_crs()
[docs] def _check_resolution(self): # not is use for now, future scope when user up/down scales the resolution """Check if the resolution meets the conditions and issue warnings.""" self.resolution=self.get_cell_resolution() # Default resolution if none provided if self.resolution is None: self.resolution = {'dx': 0.25, 'dy': 0.25} dx = self.resolution.get('dx', 0.25) dy = self.resolution.get('dy', 0.25) # Check if dx and dy are not the same if dx != dy: warnings.warn(f">> Resolution mismatch: dx ({dx}) and dy ({dy}) are not equal.\n Check 'dx' and 'dy' values of 'grid_cell_resolution' key in user config ", UserWarning) # Check if dx or dy are lower than 0.25 if dx < 0.25 or dy < 0.25: warnings.warn(f">> Resolution too fine: dx ({dx}) or dy ({dy}) is lower than weather data resolution (0.25x0.25). Consider increasing it.", UserWarning)
[docs] def generate_coords(self): # Get bounding box and actual boundary from parent class method self.bounding_box, self.actual_boundary = self.gadmBoundary.get_bounding_box() """Generate the coordinates for the grid points (centroids).""" minx, maxx = self.bounding_box['minx'], self.bounding_box['maxx'] miny, maxy = self.bounding_box['miny'], self.bounding_box['maxy'] # Use resolution from the dictionary for x and y resolution_x = self.resolution['dx'] resolution_y = self.resolution['dy'] x_values = np.arange(minx-resolution_x, maxx+resolution_x, resolution_x) y_values = np.arange(miny-resolution_x, maxy+resolution_x, resolution_y) self.coords = {"x": x_values, "y": y_values} self.shape = (len(y_values), len(x_values)) # shape as (rows, columns)
[docs] def __get_grid__(self): """ Grid with coordinates and geometries. The coordinates represent the centers of the grid cells. * Adopted from atlite.Cutout.grid method. Returns ------- geopandas.GeoDataFrame DataFrame with coordinate columns 'x' and 'y', and geometries of the corresponding grid cells. """ if not hasattr(self, 'coords'): self.generate_coords() # Create mesh grid of coordinates xs, ys = np.meshgrid(self.coords["x"], self.coords["y"]) coords = np.asarray((np.ravel(xs), np.ravel(ys))).T # Calculate span to determine grid cell size span = (coords[self.shape[1] + 1] - coords[0]) / 2 # Generate grid cells (boxes) cells = [box(*c) for c in np.hstack((coords - span, coords + span))] self.bounding_box_grid=gpd.GeoDataFrame( {"x": coords[:, 0], "y": coords[:, 1], "geometry": cells}, crs=self.crs, ) # Return GeoDataFrame with centroids and grid cells return self.bounding_box_grid
# def get_custom_grid(self): # self.bounding_box_grid=self.__get_grid__() # _grid_cells_=self.boundary_region.overlay(self.bounding_box_grid,how='intersection',keep_geom_type=True) # self.grid_cells=utils.assign_cell_id(_grid_cells_) # self.datahandler.to_store(self.grid_cells,'cells',force_update_key=True) # utils.print_update(level=2,message=f"{len(self.grid_cells)} Grid Cells prepared for {self.region_short_code}.") # return self.grid_cells
[docs] def get_default_grid(self): self.cutout,self.region_boundary=self.ERA5Cutout.get_era5_cutout() _era5_grid_cells_gdf_=self.cutout.grid _resource_grid_cells_gdf_=_era5_grid_cells_gdf_.overlay(self.region_boundary) self.resource_grid_cells=utils.assign_cell_id(_resource_grid_cells_gdf_, source_column=self.gadmBoundary.sub_national_unit_tag) self.datahandler.to_store(self.resource_grid_cells,'cells') self.datahandler.to_store(self.region_boundary,'boundary') return self.resource_grid_cells