# -*- coding: utf-8 -*-
"""
Competitive Wind Farms exclusion handler
"""
import logging
import numpy as np
from rex.utilities.utilities import parse_table
from reV.utilities import SupplyCurveField
logger = logging.getLogger(__name__)
[docs]class CompetitiveWindFarms:
"""
Handle competitive wind farm exclusion during supply curve sorting
"""
def __init__(self, wind_dirs, sc_points, n_dirs=2, offshore=False):
"""
Parameters
----------
wind_dirs : pandas.DataFrame | str
path to .csv or reVX.wind_dirs.wind_dirs.WindDirs output with
the neighboring supply curve point gids and power-rose value at
each cardinal direction
sc_points : pandas.DataFrame | str
Supply curve point summary table
n_dirs : int, optional
Number of prominent directions to use, by default 2
offshore : bool
Flag as to whether offshore farms should be included during
CompetitiveWindFarms
"""
self._wind_dirs = self._parse_wind_dirs(wind_dirs)
self._sc_gids, self._sc_point_gids, self._mask = self._parse_sc_points(
sc_points, offshore=offshore
)
self._offshore = offshore
valid = np.isin(self.sc_point_gids, self._wind_dirs.index)
if not np.all(valid):
msg = (
"'sc_points contains sc_point_gid values that do not "
"correspond to valid 'wind_dirs' sc_point_gids:\n{}".format(
self.sc_point_gids[~valid]
)
)
logger.error(msg)
raise RuntimeError(msg)
mask = self._wind_dirs.index.isin(self._sc_point_gids.keys())
self._wind_dirs = self._wind_dirs.loc[mask]
self._upwind, self._downwind = self._get_neighbors(
self._wind_dirs, n_dirs=n_dirs
)
def __repr__(self):
gids = len(self._upwind)
# pylint: disable=unsubscriptable-object
neighbors = len(self._upwind.values[0])
msg = "{} with {} sc_point_gids and {} prominent directions".format(
self.__class__.__name__, gids, neighbors
)
return msg
def __getitem__(self, keys):
"""
Map gid for given mapping
Parameters
----------
keys : tuple
(gid(s) to extract, gid) pair
Returns
-------
gid(s) : int | list
Mapped gid(s) for given mapping
"""
if not isinstance(keys, tuple):
msg = ("{} must be a tuple of form (source, gid) where source is: "
"{}, '{}', or 'upwind', 'downwind'"
.format(keys, SupplyCurveField.SC_GID,
SupplyCurveField.SC_POINT_GID))
logger.error(msg)
raise ValueError(msg)
source, gid = keys
if source == SupplyCurveField.SC_POINT_GID:
out = self.map_sc_gid_to_sc_point_gid(gid)
elif source == SupplyCurveField.SC_GID:
out = self.map_sc_point_gid_to_sc_gid(gid)
elif source == "upwind":
out = self.map_upwind(gid)
elif source == "downwind":
out = self.map_downwind(gid)
else:
msg = ("{} must be: {}, {}, or 'upwind', "
"'downwind'".format(source, SupplyCurveField.SC_GID,
SupplyCurveField.SC_POINT_GID))
logger.error(msg)
raise ValueError(msg)
return out
@property
def mask(self):
"""
Supply curve point boolean mask, used for efficient exclusion
False == excluded sc_point_gid
Returns
-------
ndarray
"""
return self._mask
@property
def sc_point_gids(self):
"""
Un-masked sc_point_gids
Returns
-------
ndarray
"""
sc_point_gids = np.array(list(self._sc_point_gids.keys()), dtype=int)
mask = self.mask[sc_point_gids]
return sc_point_gids[mask]
@property
def sc_gids(self):
"""
Un-masked sc_gids
Returns
-------
ndarray
"""
sc_gids = np.concatenate(
[self._sc_point_gids[gid] for gid in self.sc_point_gids]
)
return sc_gids
@staticmethod
def _parse_table(table):
"""
Extract features and their capacity from supply curve transmission
mapping table
Parameters
----------
table : str | pd.DataFrame
Path to .csv or .json or DataFrame to parse
Returns
-------
table : pandas.DataFrame
DataFrame extracted from file path
"""
try:
table = parse_table(table)
except ValueError as ex:
logger.error(ex)
raise
return table
@classmethod
def _parse_wind_dirs(cls, wind_dirs):
"""
Parse prominent direction neighbors
Parameters
----------
wind_dirs : pandas.DataFrame | str
Neighboring supply curve point gids and power-rose value at each
cardinal direction
Returns
-------
wind_dirs : pandas.DataFrame
Neighboring supply curve point gids and power-rose value at each
cardinal direction for each sc point gid
"""
wind_dirs = cls._parse_table(wind_dirs)
wind_dirs = wind_dirs.rename(
columns=SupplyCurveField.map_from_legacy())
wind_dirs = wind_dirs.set_index(SupplyCurveField.SC_POINT_GID)
columns = [c for c in wind_dirs if c.endswith(('_gid', '_pr'))]
wind_dirs = wind_dirs[columns]
return wind_dirs
@classmethod
def _parse_sc_points(cls, sc_points, offshore=False):
"""
Parse supply curve point summary table into sc_gid to sc_point_gid
mapping and vis-versa.
Parameters
----------
sc_points : pandas.DataFrame | str
Supply curve point summary table
offshore : bool
Flag as to whether offshore farms should be included during
CompetitiveWindFarms
Returns
-------
sc_gids : pandas.DataFrame
sc_gid to sc_point_gid mapping
sc_point_gids : pandas.DataFrame
sc_point_gid to sc_gid mapping
mask : ndarray
Mask array to mask excluded sc_point_gids
"""
sc_points = cls._parse_table(sc_points)
sc_points = sc_points.rename(
columns=SupplyCurveField.map_from_legacy())
if SupplyCurveField.OFFSHORE in sc_points and not offshore:
logger.debug('Not including offshore supply curve points in '
'CompetitiveWindFarm')
mask = sc_points[SupplyCurveField.OFFSHORE] == 0
sc_points = sc_points.loc[mask]
mask = np.ones(int(1 + sc_points[SupplyCurveField.SC_POINT_GID].max()),
dtype=bool)
sc_points = sc_points[[SupplyCurveField.SC_GID,
SupplyCurveField.SC_POINT_GID]]
sc_gids = sc_points.set_index(SupplyCurveField.SC_GID)
sc_gids = {k: int(v[0]) for k, v in sc_gids.iterrows()}
groups = sc_points.groupby(SupplyCurveField.SC_POINT_GID)
sc_point_gids = groups[SupplyCurveField.SC_GID].unique().to_frame()
sc_point_gids = {int(k): v[SupplyCurveField.SC_GID]
for k, v in sc_point_gids.iterrows()}
return sc_gids, sc_point_gids, mask
@staticmethod
def _get_neighbors(wind_dirs, n_dirs=2):
"""
Parse prominent direction neighbors
Parameters
----------
wind_dirs : pandas.DataFrame | str
Neighboring supply curve point gids and power-rose value at each
cardinal direction for each available sc point gid
n_dirs : int, optional
Number of prominent directions to use, by default 2
Returns
-------
upwind : pandas.DataFrame
Upwind neighbor gids for n prominent wind directions
downwind : pandas.DataFrame
Downwind neighbor gids for n prominent wind directions
"""
cols = [
c
for c in wind_dirs
if (c.endswith("_gid") and not c.startswith("sc"))
]
directions = [c.split("_")[0] for c in cols]
upwind_gids = wind_dirs[cols].values
cols = ["{}_pr".format(d) for d in directions]
neighbor_pr = wind_dirs[cols].values
neighbors = np.argsort(neighbor_pr)[:, :n_dirs]
upwind_gids = np.take_along_axis(upwind_gids, neighbors, axis=1)
downwind_map = {
"N": "S",
"NE": "SW",
"E": "W",
"SE": "NW",
"S": "N",
"SW": "NE",
"W": "E",
"NW": "SE",
}
cols = ["{}_gid".format(downwind_map[d]) for d in directions]
downwind_gids = wind_dirs[cols].values
downwind_gids = np.take_along_axis(downwind_gids, neighbors, axis=1)
downwind = {}
upwind = {}
for i, gid in enumerate(wind_dirs.index.values):
downwind[gid] = downwind_gids[i]
upwind[gid] = upwind_gids[i]
return upwind, downwind
[docs] def map_sc_point_gid_to_sc_gid(self, sc_point_gid):
"""
Map given sc_point_gid to equivalent sc_gid(s)
Parameters
----------
sc_point_gid : int
Supply curve point gid to map to equivalent supply curve gid(s)
Returns
-------
int | list
Equivalent supply curve gid(s)
"""
return self._sc_point_gids[sc_point_gid]
[docs] def map_sc_gid_to_sc_point_gid(self, sc_gid):
"""
Map given sc_gid to equivalent sc_point_gid
Parameters
----------
sc_gid : int
Supply curve gid to map to equivalent supply point curve gid
Returns
-------
int
Equivalent supply point curve gid
"""
return self._sc_gids[sc_gid]
[docs] def check_sc_gid(self, sc_gid):
"""
Check to see if sc_gid is valid, if so return associated
sc_point_gids
Parameters
----------
sc_gid : int
Supply curve gid to map to equivalent supply point curve gid
Returns
-------
int | None
Equivalent supply point curve gid or None if sc_gid is invalid
(offshore)
"""
sc_point_gid = None
if sc_gid in self._sc_gids:
sc_point_gid = self._sc_gids[sc_gid]
return sc_point_gid
[docs] def map_upwind(self, sc_point_gid):
"""
Map given sc_point_gid to upwind neighbors
Parameters
----------
sc_point_gid : int
Supply point curve gid to get upwind neighbors
Returns
-------
int | list
upwind neighborings
"""
return self._upwind[sc_point_gid]
[docs] def map_downwind(self, sc_point_gid):
"""
Map given sc_point_gid to downwind neighbors
Parameters
----------
sc_point_gid : int
Supply point curve gid to get downwind neighbors
Returns
-------
int | list
downwind neighborings
"""
return self._downwind[sc_point_gid]
[docs] def exclude_sc_point_gid(self, sc_point_gid):
"""
Exclude supply curve point gid, return False if gid is not present
in list of available gids to avoid key errors elsewhere
Parameters
----------
sc_point_gid : int
supply curve point gid to mask
Returns
-------
bool
Flag if gid is valid and was masked
"""
if sc_point_gid in self._sc_point_gids:
self._mask[sc_point_gid] = False
out = True
else:
out = False
return out
[docs] def remove_noncompetitive_farm(
self, sc_points, sort_on="total_lcoe", downwind=False
):
"""
Remove neighboring sc points for given number of prominent wind
directions
Parameters
----------
sc_points : pandas.DataFrame | str
Supply curve point summary table
sort_on : str, optional
column to sort on before excluding neighbors,
by default 'total_lcoe'
downwind : bool, optional
Flag to remove downwind neighbors as well as upwind neighbors,
by default False
Returns
-------
sc_points : pandas.DataFrame
Updated supply curve points after removing non-competative
wind farms
"""
sc_points = self._parse_table(sc_points)
sc_points = sc_points.rename(
columns=SupplyCurveField.map_from_legacy())
if SupplyCurveField.OFFSHORE in sc_points and not self._offshore:
mask = sc_points[SupplyCurveField.OFFSHORE] == 0
sc_points = sc_points.loc[mask]
sc_points = sc_points.sort_values(sort_on)
sc_point_gids = sc_points[SupplyCurveField.SC_POINT_GID].values
sc_point_gids = sc_point_gids.astype(int)
for i in range(len(sc_points)):
gid = sc_point_gids[i]
if self.mask[gid]:
upwind_gids = self["upwind", gid]
for n in upwind_gids:
self.exclude_sc_point_gid(n)
if downwind:
downwind_gids = self["downwind", gid]
for n in downwind_gids:
self.exclude_sc_point_gid(n)
sc_gids = self.sc_gids
mask = sc_points[SupplyCurveField.SC_GID].isin(sc_gids)
return sc_points.loc[mask].reset_index(drop=True)
[docs] @classmethod
def run(
cls,
wind_dirs,
sc_points,
n_dirs=2,
offshore=False,
sort_on="total_lcoe",
downwind=False,
out_fpath=None,
):
"""
Exclude given number of neighboring Supply Point gids based on most
prominent wind directions
Parameters
----------
wind_dirs : pandas.DataFrame | str
path to .csv or reVX.wind_dirs.wind_dirs.WindDirs output with
the neighboring supply curve point gids and power-rose value at
each cardinal direction
sc_points : pandas.DataFrame | str
Supply curve point summary table
n_dirs : int, optional
Number of prominent directions to use, by default 2
offshore : bool
Flag as to whether offshore farms should be included during
CompetitiveWindFarms
sort_on : str, optional
column to sort on before excluding neighbors,
by default 'total_lcoe'
downwind : bool, optional
Flag to remove downwind neighbors as well as upwind neighbors,
by default False
out_fpath : str, optional
Path to .csv file to save updated sc_points to,
by default None
Returns
-------
sc_points : pandas.DataFrame
Updated supply curve points after removing non-competative
wind farms
"""
cwf = cls(wind_dirs, sc_points, n_dirs=n_dirs, offshore=offshore)
sc_points = cwf.remove_noncompetitive_farm(
sc_points, sort_on=sort_on, downwind=downwind
)
if out_fpath is not None:
sc_points.to_csv(out_fpath, index=False)
return sc_points