import inspect
from typing import Dict
from pathlib import Path
import geopandas as gpd
import matplotlib.pyplot as plt
import pandas as pd
import xarray as xr
from atlite import ExclusionContainer
from shapely.geometry import Polygon
# Local Packages
import RES.utility as utils
from RES.atb import NREL_ATBProcessor
from RES.AttributesParser import AttributesParser
from RES.era5_cutout import ERA5Cutout
from RES.hdf5_handler import DataHandler
# from RES.AttributesParser import AttributesParser
from RES.lands import LandContainer
PRINT_LEVEL_BASE=2
[docs]
class CellCapacityProcessor(AttributesParser):
"""
Renewable energy capacity processor for grid cell-based resource assessment.
This class processes renewable energy potential capacity at the grid cell level by
integrating climate data, land availability constraints, and techno-economic parameters.
It calculates potential capacity matrices for solar and wind resources, applies land-use
exclusions, and generates cost-attributed capacity datasets for energy system modeling.
The class serves as the core processing engine for renewable energy resource assessment,
combining spatial analysis, climate data processing, and economic modeling to produce
grid cell-level capacity estimates suitable for energy planning and optimization models.
INHERITED METHODS FROM AttributesParser:
----------------------------------------
- get_resource_disaggregation_config() -> Dict[str, dict]: Get resource-specific config
- get_cutout_config() -> Dict[str, dict]: Get ERA5 cutout configuration
- get_gadm_config() -> Dict[str, dict]: Get GADM boundary configuration
- get_region_name() -> str: Get region name from config
- get_atb_config() -> Dict[str, dict]: Get NREL ATB cost configuration
- get_default_crs() -> str: Get default coordinate reference system
INHERITED ATTRIBUTES FROM AttributesParser:
-------------------------------------------
- config (property): Full configuration dictionary
- store (property): HDF5 store path for data persistence
- config_file_path: Path to configuration file
- region_short_code: Region identifier code
- resource_type: Resource type identifier
- Plus other configuration access methods
OWN METHODS DEFINED IN THIS CLASS:
----------------------------------
- load_cost(resource_atb): Extract and process cost parameters from ATB data
- __get_unified_region_shape__(): Create unified regional boundary geometry (private)
- __create_cell_geom__(x, y): Create grid cell geometry from coordinates (private)
- get_capacity(): Main method to process and calculate renewable energy capacity
- plot_ERAF5_grid_land_availability(): Visualize land availability on ERA5 grid
- plot_excluder_land_availability(): Visualize land availability at excluder resolution
Parameters
----------
config_file_path : str or Path
Path to configuration file containing processing settings
region_short_code : str
Region identifier for boundary and data processing
resource_type : str
Resource type ('solar', 'wind', or 'bess')
Attributes
----------
ERA5Cutout : ERA5Cutout
ERA5 climate data cutout processor instance
LandContainer : LandContainer
Land exclusion and constraint processor instance
resource_disaggregation_config : dict
Resource-specific disaggregation configuration
resource_landuse_intensity : float
Land-use intensity for capacity calculation (MW/km²)
atb : NREL_ATBProcessor
NREL Annual Technology Baseline cost data processor
datahandler : DataHandler
HDF5 data storage interface
cutout_config : dict
ERA5 cutout configuration parameters
gadm_config : dict
GADM boundary configuration parameters
disaggregation_config : dict
General disaggregation configuration
region_name : str
Full region name from configuration
utility_pv_cost : pd.DataFrame
Utility-scale PV cost data from NREL ATB
land_based_wind_cost : pd.DataFrame
Land-based wind cost data from NREL ATB
composite_excluder : ExclusionContainer
Combined land exclusion container from atlite
cell_resolution : float
Grid cell resolution in degrees
cutout : atlite.Cutout
ERA5 cutout object with climate data
region_boundary : gpd.GeoDataFrame
Regional boundary geometry
region_shape : gpd.GeoDataFrame
Unified regional shape for availability calculations
Availability_matrix : xr.DataArray
Land availability matrix from atlite
capacity_matrix : xr.DataArray
Potential capacity matrix with resource and land constraints
provincial_cells : gpd.GeoDataFrame
Final processed grid cells with capacity and cost attributes
resource_capex : float
Capital expenditure cost (million $/MW)
resource_fom : float
Fixed operation and maintenance cost (million $/MW)
resource_vom : float
Variable operation and maintenance cost (million $/MW)
grid_connection_cost_per_km : float
Grid connection cost per kilometer (million $)
tx_line_rebuild_cost : float
Transmission line rebuild cost (million $)
Methods
-------
load_cost(resource_atb: pd.DataFrame) -> tuple
Extract cost parameters from NREL ATB data and convert units
__get_unified_region_shape__() -> gpd.GeoDataFrame
Create unified regional boundary by dissolving sub-regional boundaries
__create_cell_geom__(x: float, y: float) -> Polygon
Create square grid cell geometry from center coordinates
get_capacity() -> tuple[gpd.GeoDataFrame, xr.DataArray]
Main processing method to calculate renewable energy capacity with constraints
plot_ERAF5_grid_land_availability(...) -> matplotlib.figure.Figure
Create visualization of land availability on ERA5 grid resolution
plot_excluder_land_availability(...) -> matplotlib.figure.Figure
Create visualization of land availability at excluder resolution
Examples
--------
Process solar capacity for British Columbia:
>>> from RES.CellCapacityProcessor import CellCapacityProcessor
>>> processor = CellCapacityProcessor(
... config_file_path="config/config_BC.yaml",
... region_short_code="BC",
... resource_type="solar"
... )
>>> cells_gdf, capacity_matrix = processor.get_capacity()
>>> print(f"Processed {len(cells_gdf)} cells with total capacity: "
... f"{cells_gdf['potential_capacity_solar'].sum():.1f} MW")
Process wind capacity with visualization:
>>> processor = CellCapacityProcessor(
... config_file_path="config/config_AB.yaml",
... region_short_code="AB",
... resource_type="wind"
... )
>>> cells_gdf, capacity_matrix = processor.get_capacity()
>>> # Visualizations are automatically generated and saved
Extract cost parameters:
>>> capex, vom, fom, grid_cost, tx_cost = processor.load_cost(
... processor.utility_pv_cost
... )
>>> print(f"Solar CAPEX: {capex:.3f} million $/MW")
Notes
-----
- Integrates climate data from ERA5 via atlite cutouts
- Applies land-use constraints via ExclusionContainer
- Converts NREL ATB costs from $/kW to million $/MW
- Creates square grid cells based on ERA5 resolution (~30km at 0.25°)
- Supports solar, wind, and battery energy storage systems (BESS)
- Automatically generates land availability visualizations
- Uses HDF5 storage for efficient data persistence
- Grid cells are trimmed to exact regional boundaries
- Assigns unique cell IDs for downstream processing
- Cost parameters include CAPEX, FOM, VOM, and transmission costs
Processing Workflow
-------------------
1. Load ERA5 cutout and regional boundaries
2. Set up land exclusion constraints
3. Extract cost parameters from NREL ATB
4. Calculate availability matrix with land constraints
5. Apply land-use intensity to compute capacity matrix
6. Convert to GeoDataFrame with cell geometries
7. Assign static cost parameters to each cell
8. Trim cells to precise regional boundaries
9. Generate visualizations and store results
Cost Parameter Processing
------------------------
- CAPEX: Capital expenditure (converted from $/kW to million $/MW)
- FOM: Fixed operation & maintenance (million $/MW annually)
- VOM: Variable operation & maintenance (million $/MWh, if applicable)
- Grid connection: Cost per kilometer for grid connection
- Transmission rebuild: Cost for transmission line upgrades
- Operational life: Asset lifetime (25 years solar, 20 years wind)
Dependencies
------------
- geopandas: Spatial data manipulation
- xarray: Multi-dimensional array operations
- pandas: Data frame operations
- shapely.geometry: Geometric operations
- matplotlib.pyplot: Visualization
- atlite: Climate data processing and exclusions
- RES.AttributesParser: Parent class for configuration management
- RES.lands.LandContainer: Land constraint processing
- RES.era5_cutout.ERA5Cutout: Climate data cutout handling
- RES.hdf5_handler.DataHandler: HDF5 data storage
- RES.atb.NREL_ATBProcessor: Cost data processing
- RES.utility: Utility functions for cell operations
Raises
------
KeyError
If required configuration parameters are missing
ValueError
If resource type is not supported or data processing fails
FileNotFoundError
If configuration files or data dependencies are not found
"""
def __post_init__(self):
# 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, # INHERITED ATTRIBUTE from AttributesParser
"region_short_code": self.region_short_code, # INHERITED ATTRIBUTE from AttributesParser
"resource_type": self.resource_type # INHERITED ATTRIBUTE from AttributesParser
}
self.ERA5Cutout=ERA5Cutout(**self.required_args)
self.LandContainer=LandContainer(**self.required_args)
self.resource_disaggregation_config=super().get_resource_disaggregation_config()
self.resource_landuse_intensity = self.resource_disaggregation_config['landuse_intensity']
self.atb=NREL_ATBProcessor(**self.required_args)
# Load configuration attributes that were previously inherited
self.cutout_config = super().get_cutout_config() # INHERITED METHOD from AttributesParser
self.gadm_config = super().get_gadm_config() # INHERITED METHOD from AttributesParser
self.region_name = super().get_region_name() # INHERITED METHOD from AttributesParser
(self.utility_pv_cost,
self.land_based_wind_cost,
# self.bess_cost
)= self.atb.pull_data()
## Initiate the Store and Datahandler (interfacing with the Store)
self.datahandler=DataHandler(self.store) # INHERITED ATTRIBUTE from AttributesParser
### Exclusion Layer Container
self.composite_excluder:ExclusionContainer=None
### ERA5 Cutout Resolution
self.cell_resolution=self.cutout_config['dx']
[docs]
def __get_unified_region_shape__(self):
if 'Region' in self.LandContainer.region_shape.columns:
self.region_shape=self.LandContainer.region_shape.dissolve(by=self.gadm_config['datafield_mapping']['NAME_1']) # .drop(columns =['Region'])
return self.LandContainer.region_shape
[docs]
def load_cost(self,
resource_atb:pd.DataFrame):
"""
Extracts cost parameters from the NREL ATB DataFrame and converts them to million $/MW.
Args:
resource_atb (pd.DataFrame): DataFrame containing NREL ATB cost data for the resource type.
Returns:
dict: A dictionary containing the following cost parameters:
- resource_capex: Capital expenditure in million $/MW
- resource_vom: Variable operation and maintenance cost in million $/MW
- resource_fom: Fixed operation and maintenance cost in million $/MW
- grid_connection_cost_per_km: Grid connection cost per kilometer in million $
- tx_line_rebuild_cost: Transmission line rebuild cost in million $
"""
utils.print_update(level=PRINT_LEVEL_BASE+1,
message=f"{__name__}| Extracting cost attributes...")
self.transmission_config = self.config.get('capacity_disaggregation', {}).get('transmission', {}) # INHERITED ATTRIBUTE from AttributesParser
grid_connection_cost_per_km = self.transmission_config.get('grid_connection_cost_per_Km', 0)
utils.print_info(f"{__name__}| @ Line: {inspect.currentframe().f_lineno-1} | `grid_connection_cost_per_km` is set to {grid_connection_cost_per_km} million $/km. If this is not set in the config, it will be set to 0.")
tx_line_rebuild_cost = self.disaggregation_config.get('transmission', {}).get('tx_line_rebuild_cost', 0)
utils.print_info(f"{__name__}| @ Line: {inspect.currentframe().f_lineno-1} | `grid_connection_cost_per_km` is set to {grid_connection_cost_per_km} million $/km. If this is not set in the config, it will be set to 0.")
self.ATB:Dict[str,dict]=super().get_atb_config()
source_column:str= self.ATB.get('column',{})
cost_params_mapping:Dict[str,str]=self.ATB.get('cost_params',{})
# capex,fom,vom in NREL is given in US$/kw and we need to convert it to million $/MW
resource_capex:float=resource_atb[resource_atb[source_column]==cost_params_mapping.get('capex',{})].value.iloc[0]/ 1E3 # Convert to million $/MW
resource_fom:float=resource_atb[resource_atb[source_column]==cost_params_mapping.get('fom',{})].value.iloc[0] /1E3 # Convert to million $/MW
# Initialize resource_vom based on the availability of 'vom' in cost_params_mapping
resource_vom = 0.0
if cost_params_mapping.get('vom') is not None:
# Check if the DataFrame 'utility_scale_cost' is not empty and get the value for 'vom'
if not resource_atb.empty:
vom_row = resource_atb[resource_atb[source_column] == cost_params_mapping['vom']]
if not vom_row.empty:
resource_vom = vom_row['value'].iloc[0] / 1E3 # Convert to million $/MW
cost_components:dict={
'resource_capex': resource_capex, # million $/MW
'resource_vom': resource_vom, # million $/MW
'resource_fom': resource_fom, # million $/MW
'grid_connection_cost_per_km': grid_connection_cost_per_km, # million $
'tx_line_rebuild_cost': tx_line_rebuild_cost # million $
}
# Validation
expected_keys = ['resource_capex', 'resource_vom', 'resource_fom',
'grid_connection_cost_per_km', 'tx_line_rebuild_cost']
assert all(key in cost_components for key in expected_keys), "Missing cost components"
assert all(isinstance(value, (int, float)) for value in cost_components.values()), "All values must be numeric"
# Return as ordered dictionary
return cost_components
# Define a function to create bounding boxes (of cell) directly from coordinates (x, y) and resolution
[docs]
def __create_cell_geom__(self,x, y):
half_res = self.cell_resolution / 2
return Polygon([
(x - half_res, y - half_res), # Bottom-left
(x + half_res, y - half_res), # Bottom-right
(x + half_res, y + half_res), # Top-right
(x - half_res, y + half_res) # Top-left
])
[docs]
def get_capacity(self)->tuple:
"""
This method processes the capacity of the resources based on the availability matrix and other parameters.
It calculates the potential capacity for each cell in the region and returns a named tuple containing the
processed data and the capacity matrix.
Returns:
namedtuple: A named tuple containing the processed `data` and the capacity `matrix`. Can be accessed as:
`<self.resources_nt>.data` and `<self.resources_nt>.matrix`
"""
utils.print_update(level=PRINT_LEVEL_BASE+1,
message=f"{__name__}| Cell capacity processor initiated...")
#a. load cutout and region boundary for which the cutout has been created.
self.cutout,self.region_boundary=self.ERA5Cutout.get_era5_cutout()
#b. load excluder
self.composite_excluder=self.LandContainer.set_excluder()
#d. Load costs (float)
# Load cost data as dictionary
cost_components:dict = self.load_cost(
resource_atb=(
self.utility_pv_cost if self.resource_type == 'solar' else
self.land_based_wind_cost if self.resource_type == 'wind' else
self.bess_cost if self.resource_type == 'bess' else
None
)
)
# Assign to instance variables
self.resource_capex = cost_components['resource_capex']
self.resource_vom = cost_components['resource_vom']
self.resource_fom = cost_components['resource_fom']
self.grid_connection_cost_per_km = cost_components['grid_connection_cost_per_km']
self.tx_line_rebuild_cost = cost_components['tx_line_rebuild_cost']
utils.print_update(level=PRINT_LEVEL_BASE+2,
message=f"{__name__}| ✓ Cost parameters loaded for {self.resource_type} resources.")
## 2.1 Compute availability Matrix
self.region_shape= self.__get_unified_region_shape__() # we need to pass the unified region shape to the availability matrix calculation.
utils.print_update(level=PRINT_LEVEL_BASE+1,
message=f"{__name__}| Processing Availability Matrix... ")
self.Availability_matrix:xr = self.cutout.availabilitymatrix(self.region_shape, self.composite_excluder)
utils.print_info(f"{__name__}| @ Line: {inspect.currentframe().f_lineno-1} | We need to pass the unified `region_shape` to the cutout to calculate availability for the entire region as in one of the dimensions e.g. here 'Province'. If we pass multipolygons/geoms of each Regional district (sub-provincial) we will get availability for each regional district as a dimension; which adds additional step to produce our intended data. For this analysis, one unified shape for entire region is sufficient")
utils.print_update(level=PRINT_LEVEL_BASE+2,
message=f"{__name__}| ✓ Availability Matrix processed for {self.region_name}. ")
utils.print_update(level=PRINT_LEVEL_BASE+1,
message=f"{__name__}| Creating visuals for land-availability")
self.plot_ERAF5_grid_land_availability()
self.plot_excluder_land_availability(excluder=self.composite_excluder)
area = self.cutout.grid.set_index(["y", "x"]).to_crs(3035).area / 1e6 # This crs is fit for area calculation
area = xr.DataArray(area, dims=("spatial"))
utils.print_update(level=PRINT_LEVEL_BASE+1,
message=f"{__name__}| Calculating capacity matrix, using land-use intensity for {self.resource_type} resources: {self.resource_landuse_intensity} MW/km²")
capacity_matrix:xr.DataArray = self.Availability_matrix.stack(spatial=["y", "x"]) * area * self.resource_landuse_intensity
self.capacity_matrix=capacity_matrix.rename(f'potential_capacity_{self.resource_type}')
utils.print_update(level=PRINT_LEVEL_BASE+2,
message=f"{__name__}| ✓ Capacity Matrix processed for {self.region_name}. ")
## 2.1 convert the Availability Matrix to dataframe.
# _provincial_cell_capacity_df:pd.DataFrame=self.capacity_matrix.to_dataframe()
_df_flat:pd.DataFrame=self.capacity_matrix.to_dataframe()
# _df_flat = _df_flat.drop(columns=['x', 'y'])
_df_flat = _df_flat.reset_index(drop=True)
# _df_flat = _df_flat.drop(columns='dim_0') # optional
# _df_flat = _df_flat.drop_duplicates(subset=['y', 'x'], keep='first')
# filter the cells that has no lands (i.e. no potential capacity)
# _provincial_cell_capacity_df = _provincial_cell_capacity[_provincial_cell_capacity["potential_capacity"] != 0]
# The xarray doesn't create cell geometries by default. We hav to create it.
# Apply the bounding box (cell) creation to the DataFrame's x,y coordinates (centroid of the cells)
_provincial_cell_capacity_gdf:gpd.GeoDataFrame = gpd.GeoDataFrame(
_df_flat,
geometry=[self.__create_cell_geom__(x, y) for x, y in zip(_df_flat['x'], _df_flat['y'])],
crs=super().get_default_crs() # INHERITED METHOD from AttributesParser
)
## 3 Assign Static exogenous Costs after potential capacity calculation
parameters_to_add = {
'capex': self.resource_capex, # m$/MW
'fom': self.resource_fom, # m$/MW
'vom': round(self.resource_vom, 4), # m$/MW
'grid_connection_cost_per_km': self.grid_connection_cost_per_km, # m$/km
'tx_line_rebuild_cost': self.tx_line_rebuild_cost, # m$/km
'Operational_life': int(25) if self.resource_type == 'solar' else int(20) if self.resource_type == 'wind' else 0 # years
}
# Create a new dictionary with stylized keys
stylized_columns = {f'{key}_{self.resource_type}': value for key, value in parameters_to_add.items()}
# Assign the new stylized columns to the DataFrame
_provincial_cell_capacity_gdf = _provincial_cell_capacity_gdf.assign(**stylized_columns)
## 4 Trim the cells to sub-provincial boundaries instead of overlapping cell (boxes) in the regional boundaries.
_provincial_cell_capacity_gdf=_provincial_cell_capacity_gdf.overlay(self.region_boundary)
# print(_provincial_cell_capacity_gdf.columns) # debugging purpose
self.provincial_cells=utils.assign_cell_id(cells=_provincial_cell_capacity_gdf,
source_column=self.gadm_config['datafield_mapping'].get('NAME_2'))
cells_with_capacity = self.provincial_cells
utils.print_update(level=PRINT_LEVEL_BASE+2,
message=f"{__name__}| ✓ Capacity dataframe cleaned and processed")
utils.print_update(level=PRINT_LEVEL_BASE+1,
message=f"{__name__}| ERA5 cells' capacity loaded for : {len(self.provincial_cells)} Cells [each with .025 deg. (~30km) resolution ]")
self.datahandler.to_store(self.provincial_cells,'cells')
return cells_with_capacity,capacity_matrix
## Visuals
[docs]
def plot_ERAF5_grid_land_availability(self,
region_boundary:gpd.GeoDataFrame=None,
Availability_matrix:xr.DataArray=None,
figsize=(8, 6),
legend_box_x_y:tuple=(1.2, 1)):
"""
Plots the land availability based on the ERA5 grid cells.
Args:
region_boundary (gpd.GeoDataFrame, optional): The region boundary to plot. If
not provided, the default region boundary will be used.
Availability_matrix (xr.DataArray, optional): The availability matrix to plot.
If not provided, the default Availability matrix will be used.
figsize (tuple, optional): The size of the figure to create. Defaults to (8, 6).
legend_box_x_y (tuple, optional): The position of the legend box in the plot.
Defaults to (1.2, 1).
Returns:
fig (matplotlib.figure.Figure): The figure object containing the plot.
"""
if region_boundary is None:
utils.print_update(level=PRINT_LEVEL_BASE+2,
message=f"{__name__}| No region boundary provided, using the default region boundary.")
# Use the default region boundary if not provided
region_boundary = self.region_boundary
else:
utils.print_update(level=PRINT_LEVEL_BASE+2,
message=f"{__name__}| Using provided region boundary for plotting.")
# Ensure the region boundary is in the correct CRS
if region_boundary.crs is None or region_boundary.crs.to_string() != 'EPSG:4326' :
utils.print_update(level=PRINT_LEVEL_BASE+2,
message=f"{__name__}| Region boundary CRS is None, setting to EPSG:4326.")
region_boundary = region_boundary.set_crs('EPSG:4326')
# Load availability data
if Availability_matrix is None:
utils.print_update(level=PRINT_LEVEL_BASE+2,
message=f"{__name__}| No Availability matrix provided, using the default Availability matrix.")
# Use the default Availability matrix if not provided
Availability_matrix:xr.DataArray = self.Availability_matrix
else:
utils.print_update(level=PRINT_LEVEL_BASE+2,
message=f"{__name__}| Using provided Availability matrix for plotting.")
Availability_matrix:xr.DataArray = Availability_matrix
Availability_df=Availability_matrix.to_dataframe(name="availability").reset_index()
# Define bins and labels
bins = [x / 100 for x in [0, 10, 30, 60, 90, 100]] # Define bin edges
labels = ["0-10%", "10-30%", "30-60%", "60-90%", ">90%"]
# Categorize availability into bins
Availability_df["availability_category"] = pd.cut(Availability_df["availability"], bins=bins, labels=labels, include_lowest=True)
# Convert to GeoDataFrame
A_gdf:gpd.GeoDataFrame = gpd.GeoDataFrame(
Availability_df,
geometry=[self.__create_cell_geom__(x, y) for x, y in zip(Availability_df['x'], Availability_df['y'])],
crs='EPSG:4326'
)
A_gdf=A_gdf.overlay(region_boundary)
# Categorize availability into bins
A_gdf["availability_category"] = pd.cut(A_gdf["availability"], bins=bins, labels=labels, include_lowest=True)
# Create figure and axes for side-by-side plotting
fig, ax = plt.subplots(figsize=figsize,constrained_layout=True)
# Set axis off for both subplots
ax.set_axis_off()
# Shadow effect offset
shadow_offset = 0.004
# Plot solar map on ax1
# Add shadow effect for solar map
region_boundary.geometry = region_boundary.geometry.translate(xoff=shadow_offset, yoff=-shadow_offset)
region_boundary.plot(ax=ax, facecolor='none', edgecolor='gray', linewidth=2, alpha=0.3) # Shadow layer
# Plot solar cells
A_gdf.plot(column='availability_category', ax=ax, cmap='Greens', legend=True,
legend_kwds={'title': "Land Availability", 'loc': 'upper right', 'bbox_to_anchor': legend_box_x_y,'borderpad': 1,'frameon': False})
# Plot actual boundary for solar map
region_boundary.plot(ax=ax, facecolor='none', edgecolor='black', linewidth=0.2, alpha=0.9)
plt.subplots_adjust(right=0.85) # Increase space on the right
ax.set_title(f"Land Availability for {self.resource_type} resources ({self.region_name})", fontsize=14)
# Adjust layout for cleaner appearance
plt.tight_layout()
plt.savefig(f'vis/{self.region_short_code}/lands/land_availability_ERA5grid_{self.region_short_code}_{self.resource_type}.png',dpi=300)
utils.print_update(level=PRINT_LEVEL_BASE+3,message=f"{__name__}|Land availability (grid cells) map saved at vis/{self.region_short_code}/lands/land_availability_ERA5grid_{self.region_short_code}.png")
# return fig
[docs]
def plot_excluder_land_availability(self,
excluder:ExclusionContainer=None):
"""
Plots the land availability based on the excluder resolution.
Args:
excluder (ExclusionContainer, optional): The excluder to use for plotting
Returns:
fig (matplotlib.figure.Figure): The figure object containing the plot
"""
if excluder is None:
excluder = self.composite_excluder
fig, ax = plt.subplots(figsize=(9, 6),constrained_layout=True)
excluder.plot_shape_availability(self.region_shape,
plot_kwargs={'facecolor':'none','edgecolor':'black'},
ax=ax)
ax.axis("off")
plt.savefig(f'vis/misc/land_availability_excluderResolution_{self.region_name}.png',dpi=300)
utils.print_update(level=PRINT_LEVEL_BASE+3,message=f"{__name__}|Land availability map (excluder resolution) saved at vis/misc/land_availability_excluderResolution_{self.region_name}.png")
return fig
@staticmethod
def get_sub_nationally_aggregated_capacity(cells_with_capacity:gpd.GeoDataFrame=None,
sub_national_unit_tag:str=None):
if cells_with_capacity is None or not isinstance(cells_with_capacity, gpd.GeoDataFrame):
raise ValueError("The input must be a valid GeoDataFrame with capacity data.")
if 'potential_capacity_solar' not in cells_with_capacity.columns or 'potential_capacity_wind' not in cells_with_capacity.columns:
raise ValueError("The input GeoDataFrame must contain 'potential_capacity_solar' and 'potential_capacity_wind' columns.")
if sub_national_unit_tag is None or sub_national_unit_tag not in cells_with_capacity.columns:
raise ValueError("The input GeoDataFrame must contain 'sub_national_unit_tag' column for aggregation.")
cells_aggr=cells_with_capacity.groupby(sub_national_unit_tag).aggregate({'potential_capacity_solar':'sum','potential_capacity_wind':'sum'})
cells_aggr[['potential_capacity_solar', 'potential_capacity_wind']] = cells_aggr[['potential_capacity_solar', 'potential_capacity_wind']].round().astype(int)
return cells_aggr