Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/ISSUE_TEMPLATE/bug_report.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -77,5 +77,6 @@ body:
id: charge-code
attributes:
label: Charge code
placeholder: >
If you are at NREL and fixing this bug is urgent, please provide a charge code
options:
- label: >
I am at NLR and am willing to provide a charge code to have this bug fixed urgently.
5 changes: 3 additions & 2 deletions .github/ISSUE_TEMPLATE/feature_request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,6 @@ body:
id: charge-code
attributes:
label: Charge code
placeholder: >
If you are at NREL and implementing this feature is urgent, please provide a charge code
options:
- label: >
I am at NLR and am willing to provide a charge code to have this feature implemented urgently.
4 changes: 2 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,6 @@ Recommended Citation

Update with current version and DOI:

Michael Rossol, Grant Buster, and Robert Spencer. The Renewable Energy
Potential(V) eXchange Tool: reVX. https://github.com/NREL/reVX
Michael Rossol, Grant Buster, and Robert Spencer.
The reV eXchange Tool: reVX. https://github.com/NREL/reVX
(version v0.3.20), 2021. https://doi.org/10.5281/zenodo.4507580.
4 changes: 2 additions & 2 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

project = 'reVX'
copyright = '2020, Alliance for Sustainable Energy, LLC'
author = 'NREL: Michael Rossol, Grant Buster'
author = 'NLR: Michael Rossol, Grant Buster'

pkg = os.path.dirname(os.path.abspath(os.path.dirname(__file__)))
pkg = os.path.dirname(pkg)
Expand Down Expand Up @@ -117,7 +117,7 @@

html_context = {
"display_github": True,
"github_user": "nrel",
"github_user": "nlr",
"github_repo": "reVX",
"github_version": "main",
"conf_py_path": "/docs/source/",
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ build-backend = 'setuptools.build_meta'
[project]
name="NREL-reVX"
dynamic = ["version"]
description = "National Renewable Energy Laboratory's (NREL's) Renewable Energy Potential(V) eXchange Tool: reVX"
description = "National Laboratory of the Rockies' (NLR's) reV eXchange Tool: reVX"
readme = {file = "README.rst", content-type = "text/x-rst"}
authors = [
{name = "Michael Rossol", email = "michael.rossol@nrel.gov"},
Expand All @@ -18,7 +18,7 @@ maintainers = [
{name = "Paul Pinchuk", email = "ppinchuk@nrel.gov"},
]
license = "BSD-3-Clause"
keywords = ["reVX", "NREL"]
keywords = ["reVX", "NLR"]
requires-python = ">= 3.9"
classifiers=[
"Development Status :: 4 - Beta",
Expand Down
8 changes: 5 additions & 3 deletions reVX/setbacks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,11 @@ respectively. Suitable options for ``log_level`` are ``DEBUG`` (most verbose),
``WARNING`` (only log warnings and errors), and ``ERROR`` (only log errors).

The next important parameter is ``excl_fpath``. This key must be a path that points to a template exclusions file
(path relative to the project directory are allowed). This file defines the raster grid - it must contain a county
FIPS layer called ``cnty_fips``. This layer is used to match local regulations in ``regulations_fpath`` to counties
on the grid.
(path relative to the project directory are allowed). This file defines the raster grid - it must contain geospatial
profile information that determines the shape and projection of the output exclusion arrays. If you are providing a
``regulations_fpath`` input and this input is not a GeoPackage, then the ``excl_fpath`` must contain a county FIPS
layer called ``cnty_fips``. This layer is then used to match local regulations in ``regulations_fpath`` to counties
on the grid (using the ``"FIPS"`` column in ``regulations_fpath``).

If you are running setbacks for a particular wind turbine, fill out the ``hub_height`` and ``rotor_diameter`` inputs,
and **delete the ``base_setback_dist`` input**. ``reVX`` setbacks calculations do not allow ``base_setback_dist`` if the
Expand Down
3 changes: 1 addition & 2 deletions reVX/setbacks/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,8 +385,7 @@ def compute_local_exclusions(regulation_value, county, *args):
"""
(features_ids, features_fp, col, crs, features_filter_type,
buffer_type, rasterizer) = args
logger.debug('- Computing setbacks for county FIPS {}'
.format(county.iloc[0]['FIPS']))
logger.debug('- Computing setbacks for:\n{}'.format(county.iloc[0]))
log_mem(logger)
features = _load_features(features_ids, features_fp, col, crs)
feature_bounds = _buffered_feature_bounds(features, rasterizer,
Expand Down
8 changes: 8 additions & 0 deletions reVX/setbacks/regulations.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ def __init__(self, base_setback_dist, regulations_fpath=None,
required). This is used to match the county
regulations to the county's spatial extent.

.. NOTE:: This column is optional if the regulations
file already includes a ``geometry`` column that
defines the spatial boundaries for each row.

Valid options for the ``Value Type`` are (case-insensitive;
dashes, underscores, and spaces are interchangeable):

Expand Down Expand Up @@ -203,6 +207,10 @@ def __init__(self, hub_height, rotor_diameter, regulations_fpath=None,
required). This is used to match the county
regulations to the county's spatial extent.

.. NOTE:: This column is optional if the regulations
file already includes a ``geometry`` column that
defines the spatial boundaries for each row.

Valid options for the ``Value Type`` are (case-insensitive;
dashes, underscores, and spaces are interchangeable):

Expand Down
6 changes: 4 additions & 2 deletions reVX/setbacks/setbacks_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,8 +249,10 @@ def compute_setbacks(excl_fpath, node_feature_type, node_file_path,
Parameters
----------
excl_fpath : str
Path to HDF5 file containing the county FIPS layer (should be
called ``cnty_fips``) used to match local regulations in
Path to HDF5 file containing output layer profile information.
If you are providing a ``regulations_fpath`` input that is not a
GeoPackage, this HDF5 file should also contain a county FIPS
layer (called ``cnty_fips``) used to match local regulations in
``regulations_fpath`` to counties on the grid. No data will be
written to this file unless explicitly requested via the
``out_layers`` input.
Expand Down
3 changes: 1 addition & 2 deletions reVX/turbine_flicker/turbine_flicker.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,8 +317,7 @@ def compute_local_exclusions(regulation_value, county, *args):
(hub_height, rotor_diameter, points, res_fpath,
max_flicker_exclusion_range, grid_cell_size, steps_per_hour,
building_layer, resolution) = args
logger.debug('- Computing flicker for county FIPS {}'
.format(county.iloc[0]['FIPS']))
logger.debug('- Computing flicker for:\n{}'.format(county.iloc[0]))
flicker = compute_flicker_exclusions(hub_height, rotor_diameter,
points, res_fpath,
regulation_value,
Expand Down
92 changes: 75 additions & 17 deletions reVX/utilities/exclusions.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@
import geopandas as gpd
import pandas as pd
from pyproj.crs import CRS
import rasterio
from rasterio import features as rio_features
from rasterio import windows as rio_windows
from affine import Affine
from shapely.geometry import shape

from rex import Outputs
Expand Down Expand Up @@ -145,7 +146,7 @@ def __init__(self, excl_fpath, regulations, features, hsds=False):
self._regulations = regulations
self._features = features
self._hsds = hsds
self._fips = self._profile = None
self._profile = None
self._set_profile()
self._process_regulations(regulations.df)

Expand Down Expand Up @@ -180,8 +181,37 @@ def _process_regulations(self, regulations_df):
if regulations_df is None:
return

if self._regulations.geometry_provided:
self._regulations.df = self._validate_regulations_geopackage_input(
regulations_df)
return

self._regulations.df = self._add_region_shapes_from_fips_layer(
regulations_df)

def _validate_regulations_geopackage_input(self, regulations_df):
"""Validate regulations GeoDataFrame input.="""
geometry_mask = ~regulations_df['geometry'].isna()
if not geometry_mask.any():
msg = ('Regulations were supplied with a geometry column, '
'but all geometries are null.')
logger.error(msg)
raise RuntimeError(msg)

regs_with_geom = regulations_df.loc[geometry_mask].copy()
if regs_with_geom.crs is None:
msg = ('Regulations geometries must have a defined CRS. '
'Set the CRS prior to computing exclusions.')
logger.error(msg)
raise RuntimeError(msg)

regs_with_geom = regs_with_geom.to_crs(crs=self.profile['crs'])
return regs_with_geom.reset_index(drop=True)

def _add_region_shapes_from_fips_layer(self, regulations_df):
"""Add shapes to regulations by rasterizing county FIPS layer"""
with ExclusionLayers(self._excl_fpath, hsds=self._hsds) as exc:
self._fips = exc['cnty_fips']
fips = exc['cnty_fips']
cnty_fips_profile = exc.get_layer_profile('cnty_fips')

if 'FIPS' not in regulations_df:
Expand All @@ -198,7 +228,7 @@ def _process_regulations(self, regulations_df):

logger.info('Merging county geometries w/ local regulations')
shapes_from_raster = rio_features.shapes(
self._fips.astype(np.int32),
fips.astype(np.int32),
transform=cnty_fips_profile['transform']
)
county_regs = []
Expand All @@ -218,8 +248,7 @@ def _process_regulations(self, regulations_df):
geometry='geometry'
)
regulations_df = regulations_df.reset_index()
regulations_df = regulations_df.to_crs(crs=self.profile['crs'])
self._regulations.df = regulations_df
return regulations_df.to_crs(crs=self.profile['crs'])

@property
def profile(self):
Expand Down Expand Up @@ -330,10 +359,11 @@ def compute_all_local_exclusions(self, max_workers=None):
out = self.compute_local_exclusions(exclusion_value, cnty,
*args)
local_exclusions, slices = out
fips = cnty['FIPS'].unique()
geometry = cnty.geometry.to_list()
exclusions = self._combine_exclusions(exclusions,
local_exclusions,
fips, slices)
slices,
local_geometry=geometry)
logger.debug("Computed exclusions for {:,} counties"
.format(ind))
if exclusions is None:
Expand All @@ -349,7 +379,8 @@ def _compute_local_exclusions_in_chunks(self, exe, max_submissions):
exclusion_value, cnty, args = reg
future = exe.submit(self.compute_local_exclusions,
exclusion_value, cnty, *args)
futures[future] = cnty['FIPS'].unique()
geometry = cnty.geometry.to_list()
futures[future] = geometry
if ind % max_submissions == 0:
exclusions = self._collect_local_futures(futures, exclusions)
exclusions = self._collect_local_futures(futures, exclusions)
Expand All @@ -359,10 +390,11 @@ def _collect_local_futures(self, futures, exclusions):
"""Collect all futures from the input dictionary. """
for future in as_completed(futures):
new_exclusions, slices = future.result()
geometry = futures.pop(future)
exclusions = self._combine_exclusions(exclusions,
new_exclusions,
futures.pop(future),
slices=slices)
slices=slices,
local_geometry=geometry)
log_mem(logger)
return exclusions

Expand Down Expand Up @@ -444,12 +476,13 @@ def _merge_exclusions(self, generic_exclusions, local_exclusions):
"""Merge local exclusions onto the generic exclusions."""
logger.info('Merging local exclusions onto the generic exclusions')

local_fips = self.regulations_table["FIPS"].unique()
local_geometry = self.regulations_table.geometry.to_list()
return self._combine_exclusions(generic_exclusions, local_exclusions,
local_fips, replace_existing=True)
replace_existing=True,
local_geometry=local_geometry)

def _combine_exclusions(self, existing, additional=None, cnty_fips=None,
slices=None, replace_existing=False):
def _combine_exclusions(self, existing, additional=None, slices=None,
replace_existing=False, local_geometry=None):
"""Combine local exclusions using FIPS code"""
if additional is None:
return existing
Expand All @@ -460,10 +493,11 @@ def _combine_exclusions(self, existing, additional=None, cnty_fips=None,
if slices is None:
slices = tuple([slice(None)] * len(existing.shape))

if cnty_fips is None:
if local_geometry is None:
local_exclusions = slice(None)
else:
local_exclusions = np.isin(self._fips[slices], cnty_fips)
local_exclusions = self._geometry_mask(local_geometry, slices,
additional.shape)

if replace_existing:
new_local_exclusions = additional[local_exclusions]
Expand All @@ -474,6 +508,30 @@ def _combine_exclusions(self, existing, additional=None, cnty_fips=None,
existing[slices][local_exclusions] = new_local_exclusions
return existing

def _geometry_mask(self, geometry, slices, target_shape):
"""Rasterize geometry into a boolean mask for the provided window."""
geoms = [geom for geom in geometry if geom and not geom.is_empty]
if not geoms:
return np.zeros(target_shape, dtype=bool)

array_shape = (self.profile['height'], self.profile['width'])
row_slice, col_slice = slices
window = rio_windows.Window.from_slices(row_slice, col_slice,
height=array_shape[0],
width=array_shape[1])
base_transform = self.profile['transform']
if not isinstance(base_transform, Affine):
base_transform = Affine(*base_transform)

transform = rio_windows.transform(window, base_transform)
mask = rio_features.rasterize(((geom, 1) for geom in geoms),
out_shape=target_shape,
transform=transform,
fill=0,
dtype=np.uint8)

return mask.astype(bool)

@classmethod
def run(cls, excl_fpath, features_path, out_fn, regulations,
max_workers=None, replace=False, out_layers=None, hsds=False,
Expand Down
4 changes: 2 additions & 2 deletions reVX/utilities/reeds_cols.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@ def _lowercase_alpha_only(in_str):


def add_nrel_regions(data_frame):
"""Add NREL Regions info to a Pandas DataFrame with coordinates.
"""Add NLR Regions info to a Pandas DataFrame with coordinates.

The input DataFrame must have a "state" column containing teh state
The input DataFrame must have a "state" column containing the state
name for each row.

Parameters
Expand Down
33 changes: 26 additions & 7 deletions reVX/utilities/regulations.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
class AbstractBaseRegulations(ABC):
"""ABC for county regulation values. """

REQUIRED_COLUMNS = ["Feature Type", "Value Type", "Value", "FIPS"]
_BASE_REQUIRED_COLUMNS = ["Feature Type", "Value Type", "Value"]

def __init__(self, generic_regulation_value=None, regulations_fpath=None):
"""
Expand All @@ -33,10 +33,11 @@ def __init__(self, generic_regulation_value=None, regulations_fpath=None):
represents, `Value Type`, which specifies the type of the
value (e.g. a multiplier or static height, etc.), `Value`,
which specifies the numeric value of the regulation, and
`FIPS`, which specifies a unique 5-digit code for each
county (this can be an integer - no leading zeros required).
A `None` value signifies that no local regulations should
be applied. By default `None`.
either `FIPS`, which specifies a unique 5-digit code for
each county (this can be an integer - no leading zeros
required), or a `geometry` column containing the spatial
extent for each row. A `None` value signifies that no local
regulations should be applied. By default `None`.
"""

self._generic_regulation_value = generic_regulation_value
Expand Down Expand Up @@ -93,6 +94,24 @@ def df(self, regulations_df):
self._regulations_df = regulations_df
self._validate_regulations()

@property
def geometry_provided(self):
"""bool: ``True`` if the input regulations include geometries."""
return (
isinstance(self._regulations_df, gpd.GeoDataFrame)
and "geometry" in self._regulations_df
and not self._regulations_df["geometry"].isna().all()
)

@property
def required_columns(self):
"""list: Required columns for regulations DataFrame. """
cols = self._BASE_REQUIRED_COLUMNS.copy()
if not self.geometry_provided:
cols.append("FIPS")

return cols

def _validate_regulations(self):
"""Perform several validations on regulations"""

Expand All @@ -111,7 +130,7 @@ def _convert_cols_to_title(self):

def _check_for_req_missing_cols(self):
"""Check for missing (required) columns in regulations DataFrame. """
missing = [col for col in self.REQUIRED_COLUMNS
missing = [col for col in self.required_columns
if col not in self._regulations_df]
if any(missing):
msg = ('Regulations are missing the following required columns: {}'
Expand All @@ -121,7 +140,7 @@ def _check_for_req_missing_cols(self):

def _remove_nans_from_req_cols(self):
"""Remove rows with NaN values from required columns. """
for col in self.REQUIRED_COLUMNS:
for col in self.required_columns:
na_rows = self._regulations_df[col].isna()
self._regulations_df = self._regulations_df[~na_rows]

Expand Down
2 changes: 1 addition & 1 deletion reVX/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
reVX version number
"""

__version__ = "0.5.3"
__version__ = "0.5.4"
Loading
Loading