# -*- coding: utf-8 -*-
"""
reV Project Points Configuration
"""
import copy
import logging
import os
from warnings import warn
import numpy as np
import pandas as pd
from rex.multi_file_resource import MultiFileResource
from rex.resource import Resource
from rex.resource_extraction.resource_extraction import (
MultiFileResourceX,
ResourceX,
)
from rex.utilities import check_res_file, parse_table
from reV.config.curtailment import Curtailment
from reV.config.sam_config import SAMConfig
from reV.utilities import SiteDataField, SupplyCurveField
from reV.utilities.exceptions import ConfigError, ConfigWarning
logger = logging.getLogger(__name__)
[docs]class PointsControl:
"""Class to manage and split ProjectPoints."""
def __init__(self, project_points, sites_per_split=100):
"""
Parameters
----------
project_points : reV.config.ProjectPoints
ProjectPoints instance to be split between execution workers.
sites_per_split : int
Sites per project points split instance returned in the __next__
iterator function.
"""
self._project_points = project_points
self._sites_per_split = sites_per_split
self._split_range = []
self._i = 0
self._iter_list = []
def __iter__(self):
"""Initialize the iterator by pre-splitting into a list attribute."""
last_site = 0
ilim = len(self.project_points)
logger.debug(
"PointsControl iterator initializing with sites "
"{} through {}".format(
self.project_points.sites[0], self.project_points.sites[-1]
)
)
# pre-initialize all iter objects
while True:
i0 = last_site
i1 = np.min([i0 + self.sites_per_split, ilim])
if i0 == i1:
break
last_site = i1
new = self.split(
i0,
i1,
self.project_points,
sites_per_split=self.sites_per_split,
)
new._split_range = [i0, i1]
self._iter_list.append(new)
logger.debug(
"PointsControl stopped iteration at attempted "
"index of {}. Length of iterator is: {}".format(i1, len(self))
)
return self
def __next__(self):
"""Iterate through and return next site resource data.
Returns
-------
next_pc : config.PointsControl
Split instance of this class with a subset of project points based
on the number of sites per split.
"""
if self._i < self.N:
# Get next PointsControl from the iter list
next_pc = self._iter_list[self._i]
else:
# No more points controllers left in initialized list
raise StopIteration
logger.debug(
"PointsControl passing site project points "
"with indices {} to {} on iteration #{} ".format(
next_pc.split_range[0], next_pc.split_range[1], self._i
)
)
self._i += 1
return next_pc
def __repr__(self):
msg = "{} with {} sites from gid {} through {}".format(
self.__class__.__name__,
len(self.project_points),
self.sites[0],
self.sites[-1],
)
return msg
def __len__(self):
"""Len is the number of possible iterations aka splits."""
return int(np.ceil(len(self.project_points) / self.sites_per_split))
@property
def N(self):
"""
Length of current iterator list
Returns
-------
N : int
Number of iterators in list
"""
return len(self._iter_list)
@property
def sites_per_split(self):
"""Get the iterator increment: number of sites per split.
Returns
-------
_sites_per_split : int
Sites per split iter object.
"""
return self._sites_per_split
@property
def project_points(self):
"""Get the project points property.
Returns
-------
_project_points : reV.config.project_points.ProjectPoints
ProjectPoints instance corresponding to this PointsControl
instance.
"""
return self._project_points
@property
def sites(self):
"""Get the project points sites for this instance.
Returns
-------
sites : list
List of sites belonging to the _project_points attribute.
"""
return self._project_points.sites
@property
def split_range(self):
"""Get the current split range property.
Returns
-------
_split_range : list
Two-entry list that indicates the starting and finishing
(inclusive, exclusive, respectively) indices of a split instance
of the PointsControl object. This is set in the iterator dunder
methods of PointsControl.
"""
return self._split_range
[docs] @classmethod
def split(cls, i0, i1, project_points, sites_per_split=100):
"""Split this execution by splitting the project points attribute.
Parameters
----------
i0/i1 : int
Beginning/end (inclusive/exclusive, respectively) index split
parameters for ProjectPoints.split() method.
project_points : reV.config.ProjectPoints
Project points instance that will be split.
sites_per_split : int
Sites per project points split instance returned in the __next__
iterator function.
Returns
-------
sub : PointsControl
New instance of PointsControl with a subset of the original
project points.
"""
i0 = int(i0)
i1 = int(i1)
new_points = ProjectPoints.split(i0, i1, project_points)
sub = cls(new_points, sites_per_split=sites_per_split)
return sub
[docs]class ProjectPoints:
"""Class to manage site and SAM input configuration requests.
Examples
--------
>>> import os
>>> from reV import TESTDATADIR
>>> from reV.config.project_points import ProjectPoints
>>>
>>> points = slice(0, 100)
>>> sam_file = os.path.join(TESTDATADIR, 'SAM/naris_pv_1axis_inv13.json')
>>> pp = ProjectPoints(points, sam_file)
>>>
>>> config_id_site0, SAM_config_dict_site0 = pp[0]
>>> site_list_or_slice = pp.sites
>>> site_list_or_slice = pp.get_sites_from_config(config_id)
>>> ProjectPoints_sub = pp.split(0, 10, project_points)
>>> h_list = pp.h
"""
def __init__(
self, points, sam_configs, tech=None, res_file=None, curtailment=None
):
"""
Parameters
----------
points : int | slice | list | tuple | str | pd.DataFrame | dict
Slice specifying project points, string pointing to a project
points csv, or a dataframe containing the effective csv contents.
Can also be a single integer site value.
sam_configs : dict | str | SAMConfig
SAM input configuration ID(s) and file path(s). Keys are the SAM
config ID(s) which map to the config column in the project points
CSV. Values are either a JSON SAM config file or dictionary of SAM
config inputs. Can also be a single config file path or a
pre loaded SAMConfig object.
tech : str, optional
SAM technology to analyze (pvwattsv7, windpower, tcsmoltensalt,
solarwaterheat, troughphysicalheat, lineardirectsteam)
The string should be lower-cased with spaces and _ removed,
by default None
res_file : str | NoneType
Optional resource file to find maximum length of project points if
points slice stop is None.
curtailment : NoneType | dict | str | config.curtailment.Curtailment
Inputs for curtailment parameters. If not None, curtailment inputs
are expected. Can be:
- Explicit namespace of curtailment variables (dict)
- Pointer to curtailment config json file with path (str)
- Instance of curtailment config object
(config.curtailment.Curtailment)
"""
# set protected attributes
self._df = self._parse_points(points, res_file=res_file)
self._sam_config_obj = self._parse_sam_config(sam_configs)
self._check_points_config_mapping()
self._tech = str(tech)
self._h = self._d = None
self._curtailment = self._parse_curtailment(curtailment)
def __getitem__(self, site):
"""Get the SAM config ID and dictionary for the requested site.
Parameters
----------
site : int | str
Site number (gid) of interest (typically the resource gid).
Returns
-------
config_id : str
Configuration ID (variable name) specified in the sam_generation
config section.
config : dict
Actual SAM input values in a single level dictionary with variable
names (keys) and values.
"""
site_bool = self.df[SiteDataField.GID] == site
try:
config_id = self.df.loc[site_bool, SiteDataField.CONFIG].values[0]
except (KeyError, IndexError) as ex:
msg = (
"Site {} not found in this instance of "
"ProjectPoints. Available sites include: {}".format(
site, self.sites
)
)
logger.exception(msg)
raise KeyError(msg) from ex
return config_id, copy.deepcopy(self.sam_inputs[config_id])
def __repr__(self):
msg = "{} with {} sites from gid {} through {}".format(
self.__class__.__name__, len(self), self.sites[0], self.sites[-1]
)
return msg
def __len__(self):
"""Length of this object is the number of sites."""
return len(self.sites)
@property
def df(self):
"""Get the project points dataframe property.
Returns
-------
_df : pd.DataFrame
Table of sites and corresponding SAM configuration IDs.
Has columns "gid" and 'config'.
"""
return self._df
@property
def sam_config_ids(self):
"""Get the SAM configs dictionary property.
Returns
-------
dict
Multi-level dictionary containing multiple SAM input config files.
The top level key is the SAM config ID, top level value is the SAM
config file path
"""
return sorted(self._sam_config_obj)
@property
def sam_config_obj(self):
"""Get the SAM config object.
Returns
-------
_sam_config_obj : reV.config.sam_config.SAMConfig
SAM configuration object.
"""
return self._sam_config_obj
@property
def sam_inputs(self):
"""Get the SAM configuration inputs dictionary property.
Returns
-------
dict
Multi-level dictionary containing multiple SAM input
configurations. The top level key is the SAM config ID, top level
value is the SAM config. Each SAM config is a dictionary with keys
equal to input names, values equal to the actual inputs.
"""
return self.sam_config_obj.inputs
@property
def all_sam_input_keys(self):
"""Get a list of unique input keys from all SAM technology configs.
Returns
-------
all_sam_input_keys : list
List of unique strings where each string is a input key for the
SAM technology configs. For example, "gcr" or "losses" for PVWatts
or "wind_turbine_hub_ht" for windpower.
"""
keys = []
for sam_config in self.sam_inputs.values():
keys += list(sam_config.keys())
keys = list(set(keys))
return keys
@property
def gids(self):
"""Get the list of gids (resource file index values) belonging to this
instance of ProjectPoints. This is an alias of self.sites.
Returns
-------
gids : list
List of integer gids (resource file index values) belonging to this
instance of ProjectPoints. This is an alias of self.sites.
"""
return self.sites
@property
def sites(self):
"""Get the list of sites (resource file gids) belonging to this
instance of ProjectPoints.
Returns
-------
sites : list
List of integer sites (resource file gids) belonging to this
instance of ProjectPoints.
"""
return self.df[SiteDataField.GID].values.tolist()
@property
def sites_as_slice(self):
"""Get the sites in slice format.
Returns
-------
sites_as_slice : list | slice
Sites slice belonging to this instance of ProjectPoints.
The type is slice if possible. Will be a list only if sites are
non-sequential.
"""
# try_slice is what the sites list would be if it is sequential
if len(self.sites) > 1:
try_step = self.sites[1] - self.sites[0]
else:
try_step = 1
try_slice = slice(self.sites[0], self.sites[-1] + 1, try_step)
try_list = list(range(*try_slice.indices(try_slice.stop)))
if self.sites == try_list:
# try_slice is equivelant to the site list
sites_as_slice = try_slice
else:
# cannot be converted to a sequential slice, return list
sites_as_slice = self.sites
return sites_as_slice
@property
def tech(self):
"""Get the tech property from the config.
Returns
-------
_tech : str
SAM technology to analyze (pvwattsv7, windpower, tcsmoltensalt,
solarwaterheat, troughphysicalheat, lineardirectsteam)
The string should be lower-cased with spaces and _ removed.
"""
return "windpower" if "wind" in self._tech.lower() else self._tech
@property
def h(self):
"""Get the hub heights corresponding to the site list.
Returns
-------
_h : list | NoneType
Hub heights corresponding to each site, taken from the sam config
for each site. This is None if the technology is not wind.
"""
h_var = "wind_turbine_hub_ht"
if self._h is None:
if "wind" in self.tech:
# wind technology, get a list of h values
self._h = [self[site][1][h_var] for site in self.sites]
return self._h
@property
def d(self):
"""Get the depths (m) corresponding to the site list.
Returns
-------
_d : list | NoneType
Resource depths (m) corresponding to each site, taken from
the sam config for each site. This is None if the technology
is not geothermal.
"""
d_var = "resource_depth"
if self._d is None:
if "geothermal" in self.tech:
if d_var in self.df:
self._d = list(self.df[d_var])
else:
self._d = [self[site][1][d_var] for site in self.sites]
return self._d
@property
def curtailment(self):
"""Get the curtailment config object.
Returns
-------
_curtailment : NoneType | reV.config.curtailment.Curtailment
None if no curtailment, reV curtailment config object if
curtailment is being assessed.
"""
return self._curtailment
@staticmethod
def _parse_csv(fname):
"""Import project points from .csv
Parameters
----------
fname : str
Project points .csv file (with path). Must have 'gid' and
'config' column names.
Returns
-------
df : pd.DataFrame
DataFrame mapping sites (gids) to SAM technology (config)
"""
fname = fname.strip()
if fname.endswith(".csv"):
df = pd.read_csv(fname)
else:
raise ValueError(
"Config project points file must be "
".csv, but received: {}".format(fname)
)
return df
@staticmethod
def _parse_sites(points, res_file=None):
"""Parse project points from list or slice
Parameters
----------
points : int | str | pd.DataFrame | slice | list
Slice specifying project points, string pointing to a project
points csv, or a dataframe containing the effective csv contents.
Can also be a single integer site value.
res_file : str | NoneType
Optional resource file to find maximum length of project points if
points slice stop is None.
Returns
-------
df : pd.DataFrame
DataFrame mapping sites (gids) to SAM technology (config)
"""
df = pd.DataFrame(columns=[SiteDataField.GID, SiteDataField.CONFIG])
if isinstance(points, int):
points = [points]
if isinstance(points, (list, tuple, np.ndarray)):
# explicit site list, set directly
if any(isinstance(i, (list, tuple, np.ndarray)) for i in points):
msg = "Provided project points is not flat: {}!".format(points)
logger.error(msg)
raise RuntimeError(msg)
df[SiteDataField.GID] = points
elif isinstance(points, slice):
stop = points.stop
if stop is None:
if res_file is None:
raise ValueError(
"Must supply a resource file if "
"points is a slice of type "
" slice(*, None, *)"
)
multi_h5_res, _ = check_res_file(res_file)
if multi_h5_res:
stop = MultiFileResource(res_file).shape[1]
else:
stop = Resource(res_file).shape[1]
df[SiteDataField.GID] = list(range(*points.indices(stop)))
else:
raise TypeError(
"Project Points sites needs to be set as a list, "
"tuple, or slice, but was set as: {}".format(type(points))
)
df[SiteDataField.CONFIG] = None
return df
@classmethod
def _parse_points(cls, points, res_file=None):
"""Generate the project points df from inputs
Parameters
----------
points : int | str | pd.DataFrame | slice | list | dict
Slice specifying project points, string pointing to a project
points csv, or a dataframe containing the effective csv contents.
Can also be a single integer site value.
res_file : str | NoneType
Optional resource file to find maximum length of project points if
points slice stop is None.
Returns
-------
df : pd.DataFrame
DataFrame mapping sites (gids) to SAM technology (config)
"""
if isinstance(points, str):
df = cls._parse_csv(points)
elif isinstance(points, dict):
df = pd.DataFrame(points)
elif isinstance(points, (int, slice, list, tuple, np.ndarray)):
df = cls._parse_sites(points, res_file=res_file)
elif isinstance(points, pd.DataFrame):
df = points
else:
raise ValueError(
"Cannot parse Project points data from {}".format(type(points))
)
df = df.rename(SupplyCurveField.map_to(SiteDataField), axis=1)
if SiteDataField.GID not in df.columns:
raise KeyError(
"Project points data must contain "
f"{SiteDataField.GID} column."
)
# pylint: disable=no-member
if SiteDataField.CONFIG not in df.columns:
df[SiteDataField.CONFIG] = None
gids = df[SiteDataField.GID].values
if not np.array_equal(np.sort(gids), gids):
msg = (
"WARNING: points are not in sequential order and will be "
"sorted! The original order is being preserved under "
'column "points_order"'
)
logger.warning(msg)
warn(msg)
df["points_order"] = df.index.values
df = df.sort_values(SiteDataField.GID).reset_index(drop=True)
return df
@staticmethod
def _parse_sam_config(sam_config):
"""
Create SAM files dictionary.
Parameters
----------
sam_config : dict | str | SAMConfig
SAM input configuration ID(s) and file path(s) or SAM config
dict(s). Keys are the SAM config ID(s). Can also be a single
config file str. Can also be a pre loaded SAMConfig object.
Returns
-------
_sam_config_obj : reV.config.sam_config.SAMConfig
SAM configuration object.
"""
if isinstance(sam_config, SAMConfig):
return sam_config
if isinstance(sam_config, dict):
config_dict = sam_config
elif isinstance(sam_config, str):
config_dict = {sam_config: sam_config}
else:
raise ValueError(
"Cannot parse SAM configs from {}".format(type(sam_config))
)
return SAMConfig(config_dict)
@staticmethod
def _parse_curtailment(curtailment_input):
"""Parse curtailment config object.
Parameters
----------
curtailment_input : None | dict | str | config.curtailment.Curtailment
Inputs for curtailment parameters. If not None, curtailment inputs
are expected. Can be:
- Explicit namespace of curtailment variables (dict)
- Pointer to curtailment config json file with path (str)
- Instance of curtailment config object
(config.curtailment.Curtailment)
Returns
-------
curtailments : NoneType | reV.config.curtailment.Curtailment
None if no curtailment, reV curtailment config object if
curtailment is being assessed.
"""
if isinstance(curtailment_input, (str, dict)):
# pointer to config file or explicit input namespace,
# instantiate curtailment config object
curtailment = Curtailment(curtailment_input)
elif isinstance(curtailment_input, (Curtailment, type(None))):
# pre-initialized curtailment object or no curtailment (None)
curtailment = curtailment_input
else:
curtailment = None
warn(
"Curtailment inputs not recognized. Received curtailment "
'input of type: "{}". Expected None, dict, str, or '
"Curtailment object. Defaulting to no curtailment.",
ConfigWarning,
)
return curtailment
[docs] def index(self, gid):
"""Get the index location (iloc not loc) for a resource gid found in
the project points.
Parameters
----------
gid : int
Resource GID found in the project points gid column.
Returns
-------
ind : int
Row index of gid in the project points dataframe.
"""
if gid not in self._df[SiteDataField.GID].values:
e = (
"Requested resource gid {} is not present in the project "
"points dataframe. Cannot return row index.".format(gid)
)
logger.error(e)
raise ConfigError(e)
ind = np.where(self._df[SiteDataField.GID] == gid)[0][0]
return ind
def _check_points_config_mapping(self):
"""
Check to ensure the project points (df) and SAM configs
(sam_config_obj) are compatible. Update as necessary or break
"""
# Extract unique config refences from project_points DataFrame
df_configs = self.df[SiteDataField.CONFIG].unique()
sam_configs = self.sam_inputs
# Checks to make sure that the same number of SAM config files
# as references in project_points DataFrame
if len(df_configs) > len(sam_configs):
msg = (
"Points references {} configs while only "
"{} SAM configs were provided!".format(
len(df_configs), len(sam_configs)
)
)
logger.error(msg)
raise ConfigError(msg)
if len(df_configs) == 1 and df_configs[0] is None:
self._df[SiteDataField.CONFIG] = list(sam_configs)[0]
df_configs = self.df[SiteDataField.CONFIG].unique()
# Check to see if config references in project_points DataFrame
# are valid file paths, if compare with SAM configs
# and update as needed
configs = {}
for config in df_configs:
if os.path.isfile(config):
configs[config] = config
elif config in sam_configs:
configs[config] = sam_configs[config]
else:
msg = "{} does not map to a valid configuration file".format(
config
)
logger.error(msg)
raise ConfigError(msg)
# If configs has any keys that are not in sam_configs then
# something really weird happened so raise an error.
if any(set(configs) - set(sam_configs)):
msg = (
"A wild config has appeared! Requested config keys for "
"ProjectPoints are {} and previous config keys are {}".format(
list(configs), list(sam_configs)
)
)
logger.error(msg)
raise ConfigError(msg)
[docs] def join_df(self, df2, key=SiteDataField.GID):
"""Join new df2 to the _df attribute using the _df's gid as pkey.
This can be used to add site-specific data to the project_points,
taking advantage of the points_control iterator/split functions such
that only the relevant site data is passed to the analysis functions.
Parameters
----------
df2 : pd.DataFrame
Dataframe to be joined to the self._df attribute (this instance
of project points dataframe). This likely contains
site-specific inputs that are to be passed to parallel workers.
key : str
Primary key of df2 to be joined to the _df attribute (this
instance of the project points dataframe). Primary key
of the self._df attribute is fixed as the gid column.
"""
# ensure df2 doesnt have any duplicate columns for suffix reasons.
df2_cols = [c for c in df2.columns if c not in self._df or c == key]
self._df = pd.merge(
self._df,
df2[df2_cols],
how="left",
left_on=SiteDataField.GID,
right_on=key,
copy=False,
validate="1:1",
)
[docs] def get_sites_from_config(self, config):
"""Get a site list that corresponds to a config key.
Parameters
----------
config : str
SAM configuration ID associated with sites.
Returns
-------
sites : list
List of sites associated with the requested configuration ID. If
the configuration ID is not recognized, an empty list is returned.
"""
sites = self.df.loc[
(self.df[SiteDataField.CONFIG] == config), SiteDataField.GID
].values
return list(sites)
[docs] @classmethod
def split(cls, i0, i1, project_points):
"""Return split instance of a ProjectPoints instance w/ site subset.
Parameters
----------
i0 : int
Starting INDEX (not resource gid) (inclusive) of the site property
attribute to include in the split instance. This is not necessarily
the same as the starting site number, for instance if ProjectPoints
is sites 20:100, i0=0 i1=10 will result in sites 20:30.
i1 : int
Ending INDEX (not resource gid) (exclusive) of the site property
attribute to include in the split instance. This is not necessarily
the same as the final site number, for instance if ProjectPoints is
sites 20:100, i0=0 i1=10 will result in sites 20:30.
project_points: ProjectPoints
Instance of project points to split.
Returns
-------
sub : ProjectPoints
New instance of ProjectPoints with a subset of the following
attributes: sites, project points df, and the self dictionary data
struct.
"""
# Extract DF subset with only index values between i0 and i1
n = len(project_points)
if i0 > n or i1 > n:
raise ValueError(
"{} and {} must be within the range of "
"project_points (0 - {})".format(i0, i1, n - 1)
)
points_df = project_points.df.iloc[i0:i1]
# make a new instance of ProjectPoints with subset DF
sub = cls(
points_df,
project_points.sam_config_obj,
project_points.tech,
curtailment=project_points.curtailment,
)
return sub
@staticmethod
def _parse_lat_lons(lat_lons):
msg = (
"Expecting a pair or multiple pairs of latitude and "
"longitude coordinates!"
)
if isinstance(lat_lons, str):
lat_lons = parse_table(lat_lons)
cols = [
c for c in lat_lons if c.lower().startswith(("lat", "lon"))
]
lat_lons = lat_lons[sorted(cols)].values
elif isinstance(lat_lons, (list, tuple)):
lat_lons = np.array(lat_lons)
elif isinstance(lat_lons, (int, float)):
msg += " Recieved a single coordinate value!"
logger.error(msg)
raise ValueError(msg)
if len(lat_lons.shape) == 1:
lat_lons = np.expand_dims(lat_lons, axis=0)
if lat_lons.shape[1] != 2:
msg += " Received {} coordinate values!".format(lat_lons.shape[1])
logger.error(msg)
raise ValueError(msg)
return lat_lons
[docs] @classmethod
def lat_lon_coords(
cls, lat_lons, res_file, sam_configs, tech=None, curtailment=None
):
"""
Generate ProjectPoints for gids nearest to given latitude longitudes
Parameters
----------
lat_lons : str | tuple | list | ndarray
Pair or pairs of latitude longitude coordinates
res_file : str
Resource file, needed to fine nearest neighbors
sam_configs : dict | str | SAMConfig
SAM input configuration ID(s) and file path(s). Keys are the SAM
config ID(s) which map to the config column in the project points
CSV. Values are either a JSON SAM config file or dictionary of SAM
config inputs. Can also be a single config file path or a
pre loaded SAMConfig object.
tech : str, optional
SAM technology to analyze (pvwattsv7, windpower, tcsmoltensalt,
solarwaterheat, troughphysicalheat, lineardirectsteam)
The string should be lower-cased with spaces and _ removed,
by default None
curtailment : NoneType | dict | str | config.curtailment.Curtailment
Inputs for curtailment parameters. If not None, curtailment inputs
are expected. Can be:
- Explicit namespace of curtailment variables (dict)
- Pointer to curtailment config json file with path (str)
- Instance of curtailment config object
(config.curtailment.Curtailment)
Returns
-------
pp : ProjectPoints
Initialized ProjectPoints object for points nearest to given
lat_lons
"""
lat_lons = cls._parse_lat_lons(lat_lons)
multi_h5_res, hsds = check_res_file(res_file)
if multi_h5_res:
res_cls = MultiFileResourceX
res_kwargs = {}
else:
res_cls = ResourceX
res_kwargs = {"hsds": hsds}
logger.info(
"Converting latitude longitude coordinates into nearest "
"ProjectPoints"
)
logger.debug("- (lat, lon) pairs:\n{}".format(lat_lons))
with res_cls(res_file, **res_kwargs) as f:
gids = f.lat_lon_gid(lat_lons) # pylint: disable=no-member
if isinstance(gids, int):
gids = [gids]
else:
if len(gids) != len(np.unique(gids)):
uniques, pos, counts = np.unique(
gids, return_counts=True, return_inverse=True
)
duplicates = {}
for idx in np.where(counts > 1)[0]:
duplicate_lat_lons = lat_lons[np.where(pos == idx)[0]]
duplicates[uniques[idx]] = duplicate_lat_lons
msg = (
"reV Cannot currently handle duplicate Resource gids! "
"The given latitude and longitudes map to the same "
"gids:\n{}".format(duplicates)
)
logger.error(msg)
raise RuntimeError(msg)
gids = gids.tolist()
logger.debug("- Resource gids:\n{}".format(gids))
pp = cls(
gids,
sam_configs,
tech=tech,
res_file=res_file,
curtailment=curtailment,
)
if "points_order" in pp.df:
lat_lons = lat_lons[pp.df["points_order"].values]
pp._df["latitude"] = lat_lons[:, 0]
pp._df["longitude"] = lat_lons[:, 1]
return pp
[docs] @classmethod
def regions(
cls, regions, res_file, sam_configs, tech=None, curtailment=None
):
"""
Generate ProjectPoints for gids nearest to given latitude longitudes
Parameters
----------
regions : dict
Dictionary of regions to extract points for in the form:
{'region': 'region_column'}
res_file : str
Resource file, needed to fine nearest neighbors
sam_configs : dict | str | SAMConfig
SAM input configuration ID(s) and file path(s). Keys are the SAM
config ID(s) which map to the config column in the project points
CSV. Values are either a JSON SAM config file or dictionary of SAM
config inputs. Can also be a single config file path or a
pre loaded SAMConfig object.
tech : str, optional
SAM technology to analyze (pvwattsv7, windpower, tcsmoltensalt,
solarwaterheat, troughphysicalheat, lineardirectsteam)
The string should be lower-cased with spaces and _ removed,
by default None
curtailment : NoneType | dict | str | config.curtailment.Curtailment
Inputs for curtailment parameters. If not None, curtailment inputs
are expected. Can be:
- Explicit namespace of curtailment variables (dict)
- Pointer to curtailment config json file with path (str)
- Instance of curtailment config object
(config.curtailment.Curtailment)
Returns
-------
pp : ProjectPoints
Initialized ProjectPoints object for points nearest to given
lat_lons
"""
multi_h5_res, hsds = check_res_file(res_file)
if multi_h5_res:
res_cls = MultiFileResourceX
else:
res_cls = ResourceX
logger.info("Extracting ProjectPoints for desired regions")
points = []
with res_cls(res_file, hsds=hsds) as f:
meta = f.meta
for region, region_col in regions.items():
logger.debug("- {}: {}".format(region_col, region))
# pylint: disable=no-member
gids = f.region_gids(region, region_col=region_col)
logger.debug("- Resource gids:\n{}".format(gids))
if points:
duplicates = np.intersect1d(gids, points).tolist()
if duplicates:
msg = (
"reV Cannot currently handle duplicate "
"Resource gids! The given regions containg the "
"same gids:\n{}".format(duplicates)
)
logger.error(msg)
raise RuntimeError(msg)
points.extend(gids.tolist())
pp = cls(
points,
sam_configs,
tech=tech,
res_file=res_file,
curtailment=curtailment,
)
meta = meta.loc[pp.sites]
cols = list(set(regions.values()))
for c in cols:
pp._df[c] = meta[c].values
return pp