Skip to content

Commit

Permalink
Merge pull request #449 from NREL/pp/exclusion_extent
Browse files Browse the repository at this point in the history
Exclusion extent functionality
  • Loading branch information
ppinchuk committed May 9, 2024
2 parents 8509218 + 9252c3d commit e1d6163
Show file tree
Hide file tree
Showing 4 changed files with 370 additions and 72 deletions.
195 changes: 125 additions & 70 deletions reV/supply_curve/exclusions.py
Expand Up @@ -32,6 +32,7 @@ def __init__(self, layer,
weight=1.0,
exclude_nodata=False,
nodata_value=None,
extent=None,
**kwargs):
"""
Parameters
Expand All @@ -49,39 +50,44 @@ def __init__(self, layer,
By default, ``None``.
exclude_range : list | tuple, optional
Two-item list of (min threshold, max threshold) for values
to exclude. Mutually exclusive with other inputs - see info
in the description of `exclude_values`.
By default, ``None``.
Two-item list of [min threshold, max threshold] (ends are
inclusive) for values to exclude. Mutually exclusive
with other inputs (see info in the description of
`exclude_values`). By default, ``None``.
include_values : int | float | list, optional
Single value or list of values to include. Mutually
exclusive with other inputs - see info in the description of
`exclude_values`. By default, ``None``.
exclusive with other inputs (see info in the description of
`exclude_values`). By default, ``None``.
include_range : list | tuple, optional
Two-item list of (min threshold, max threshold) for values
to include. Mutually exclusive with other inputs - see info
in the description of `exclude_values`.
By default, ``None``.
Two-item list of [min threshold, max threshold] (ends are
inclusive) for values to include. Mutually exclusive with
other inputs (see info in the description of
`exclude_values`). By default, ``None``.
include_weights : dict, optional
A dictionary of ``{value: weight}`` pairs, where the
``value`` in the layer that should be included with the
given ``weight``. Mutually exclusive with other inputs - see
info in the description of `exclude_values`.
given ``weight``. Mutually exclusive with other inputs (see
info in the description of `exclude_values`).
By default, ``None``.
force_include_values : int | float | list, optional
Force the inclusion of the given value(s). Mutually
exclusive with other inputs - see info in the description of
`exclude_values`. By default, ``None``.
Force the inclusion of the given value(s). This input
completely replaces anything provided as `include_values`
and is mutually exclusive with other inputs (eee info in
the description of `exclude_values`). By default, ``None``.
force_include_range : list | tuple, optional
Force the inclusion of given values in the range
(min threshold, max threshold). Mutually exclusive with
other inputs - see info in the description of
`exclude_values`. By default, ``None``.
[min threshold, max threshold] (ends are inclusive). This
input completely replaces anything provided as
`include_range` and is mutually exclusive with other inputs
(see info in the description of `exclude_values`).
By default, ``None``.
use_as_weights : bool, optional
Option to use layer as final inclusion weights. If ``True``,
all inclusion/exclusions specifications for the layer are
ignored and the raw values (scaled by the `weight` input)
are used as weights. By default, ``False``.
Option to use layer as final inclusion weights (i.e.
1 = fully included, 0.75 = 75% included, 0.5 = 50% included,
etc.). If ``True``, all inclusion/exclusions specifications
for the layer are ignored and the raw values (scaled by the
`weight` input) are used as inclusion weights.
By default, ``False``.
weight : float, optional
Weight applied to exclusion layer after it is calculated.
Can be used, for example, to turn a binary exclusion layer
Expand All @@ -98,6 +104,38 @@ def __init__(self, layer,
inferred when LayerMask is added to
:class:`reV.supply_curve.exclusions.ExclusionMask`.
By default, ``None``.
extent : dict, optional
Optional dictionary with values that can be used to
initialize this class (i.e. `layer`, `exclude_values`,
`include_range`, etc.). This dictionary should contain the
specifications to create a boolean mask that defines the
extent to which the original mask should be applied.
For example, suppose you specify the input the following
way:
input_dict = {
"viewsheds": {
"exclude_values": 1,
"extent": {
"layer": "federal_parks",
"include_range": [1, 5]
}
}
}
for layer_name, kwargs in input_dict.items():
layer = LayerMask(layer_name, **kwargs)
...
This would mean that you are masking out all viewshed layer
values equal to 1, **but only where the "federal_parks"
layer is equal to 1, 2, 3, 4, or 5**. Outside of these
regions (i.e. outside of federal park regions), the viewshed
exclusion is **NOT** applied. If the extent mask created by
these options is not boolean, an error is thrown (i.e. do
not specify `weight` or `use_as_weights`).
By default ``None``, which applies the original layer mask
to the full extent.
**kwargs
Optional inputs to maintain legacy kwargs of ``inclusion_*``
instead of ``include_*``.
Expand Down Expand Up @@ -125,13 +163,14 @@ def __init__(self, layer,
self.nodata_value = nodata_value

if weight > 1 or weight < 0:
msg = ('Invalide weight ({}) provided for layer {}:'
msg = ('Invalid weight ({}) provided for layer {}:'
'\nWeight must fall between 0 and 1!'.format(weight, layer))
logger.error(msg)
raise ValueError(msg)

self._weight = weight
self._mask_type = self._check_mask_type()
self.extent = LayerMask(**extent) if extent is not None else None

def __repr__(self):
msg = ('{} for "{}" exclusion, of type "{}"'
Expand Down Expand Up @@ -355,6 +394,16 @@ def _check_mask_type(self):
logger.error(msg)
raise ExclusionLayerError(msg)

if mask is None:
msg = ('Exactly one approach must be specified to create the '
'inclusion mask for layer {!r}! Please specify one of: '
'`exclude_values`, `exclude_range`, `include_values`, '
'`include_range`, `include_weights`, '
'`force_include_values`, or `force_include_range`.'
.format(self.name))
logger.error(msg)
raise ExclusionLayerError(msg)

if mask == 'include_weights' and self._weight < 1:
msg = ("Values are individually weighted when using "
"'include_weights', the supplied weight of {} will be "
Expand Down Expand Up @@ -888,34 +937,54 @@ def _generate_ones_mask(self, ds_slice):

return mask

def _force_include(self, mask, layers, ds_slice):
"""
Apply force inclusion layers
def _add_layer_to_mask(self, mask, layer, ds_slice, check_layers,
combine_func):
"""Add layer mask to full mask. """
layer_mask = self._compute_layer_mask(layer, ds_slice, check_layers)
if mask is None:
return layer_mask

Parameters
----------
mask : ndarray | None
Mask to apply force inclusion layers to
layers : list
List of force inclusion layers
ds_slice : int | slice | list | ndarray
What to extract from ds, each arg is for a sequential axis.
For example, (slice(0, 64), slice(0, 64)) will extract a 64x64
exclusions mask.
"""
for layer in layers:
layer_slice = (layer.name, ) + ds_slice
layer_mask = layer[self.excl_h5[layer_slice]]
logger.debug('Computing forced inclusions for {}. Layer has '
'average value of {:.2f}'
.format(layer, layer_mask.mean()))
log_mem(logger, log_level='DEBUG')
if mask is None:
mask = layer_mask
else:
mask = np.maximum(mask, layer_mask, dtype='float32')
return combine_func(mask, layer_mask, dtype='float32')

return mask
def _compute_layer_mask(self, layer, ds_slice, check_layers=False):
"""Compute mask for single layer, including extent. """
layer_mask = self._masked_layer_data(layer, ds_slice)
layer_mask = self._apply_layer_mask_extent(layer, layer_mask, ds_slice)

logger.debug('Computed exclusions {} for {}. Layer has average value '
'of {:.2f}.'
.format(layer, ds_slice, layer_mask.mean()))
log_mem(logger, log_level='DEBUG')

if check_layers and not layer_mask.any():
msg = "Layer {} is fully excluded!".format(layer.name)
logger.error(msg)
raise ExclusionLayerError(msg)

return layer_mask

def _apply_layer_mask_extent(self, layer, layer_mask, ds_slice):
"""Apply extent to layer mask, if any. """
if layer.extent is None:
return layer_mask

layer_extent = self._masked_layer_data(layer.extent, ds_slice)
if not np.array_equal(layer_extent, layer_extent.astype(bool)):
msg = ("Extent layer must be boolean (i.e. 0 and 1 values "
"only)! Please check your extent definition for layer "
"{} to ensure you are producing a boolean layer!"
.format(layer.name))
logger.error(msg)
raise ExclusionLayerError(msg)

logger.debug("Filtering mask for layer %s down to specified extent",
layer.name)
layer_mask = np.where(layer_extent, layer_mask, 1)
return layer_mask

def _masked_layer_data(self, layer, ds_slice):
"""Extract masked data for layer. """
return layer[self.excl_h5[(layer.name, ) + ds_slice]]

def _generate_mask(self, *ds_slice, check_layers=False):
"""
Expand Down Expand Up @@ -950,27 +1019,13 @@ def _generate_mask(self, *ds_slice, check_layers=False):
if layer.force_include:
force_include.append(layer)
else:
layer_slice = (layer.name, ) + ds_slice
layer_mask = layer[self.excl_h5[layer_slice]]

logger.debug('Computed exclusions {} for {}. '
'Layer has average value of {:.2f}.'
.format(layer, ds_slice, layer_mask.mean()))
log_mem(logger, log_level='DEBUG')

if check_layers and not layer_mask.any():
msg = ("Layer {} is fully excluded!"
.format(layer.name))
logger.error(msg)
raise ExclusionLayerError(msg)

if mask is None:
mask = layer_mask
else:
mask = np.minimum(mask, layer_mask, dtype='float32')

if force_include:
mask = self._force_include(mask, force_include, ds_slice)
mask = self._add_layer_to_mask(mask, layer, ds_slice,
check_layers,
combine_func=np.minimum)
for layer in force_include:
mask = self._add_layer_to_mask(mask, layer, ds_slice,
check_layers,
combine_func=np.maximum)

if self._min_area is not None:
mask = self._area_filter(mask, self._min_area,
Expand Down
7 changes: 7 additions & 0 deletions reV/supply_curve/sc_aggregation.py
Expand Up @@ -327,6 +327,13 @@ def __init__(self, excl_fpath, tm_dset, econ_fpath=None,
"more_developable_land": {
"force_include_range": [5, 10]
},
"viewsheds": {
"exclude_values": 1,
"extent": {
"layer": "federal_parks",
"include_range": [1, 5]
}
}
...
}
Expand Down
2 changes: 1 addition & 1 deletion reV/version.py
Expand Up @@ -2,4 +2,4 @@
reV Version number
"""

__version__ = "0.8.7"
__version__ = "0.8.8"

0 comments on commit e1d6163

Please sign in to comment.