Skip to content

Commit

Permalink
Add extent option
Browse files Browse the repository at this point in the history
  • Loading branch information
ppinchuk committed Apr 22, 2024
1 parent 6d4de77 commit 873af12
Show file tree
Hide file tree
Showing 2 changed files with 232 additions and 20 deletions.
93 changes: 74 additions & 19 deletions reV/supply_curve/exclusions.py
Original file line number Diff line number Diff line change
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,36 +50,37 @@ 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). This input
completely replaces anything provided as `include_values`
and is mutually exclusive with other inputs - see info in
the description of `exclude_values`. By default, ``None``.
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). 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``.
[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 (i.e.
1 = fully included, 0.75 = 75% included, 0.5 = 50% included,
Expand All @@ -102,6 +104,37 @@ 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 this
region (i.e. outside of federal park regions), the viewshed
exclusion is **NOT** applied. If the 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 @@ -129,13 +162,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 @@ -914,6 +948,7 @@ def _add_layer_to_mask(self, mask, layer, ds_slice, check_layers,
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}.'
Expand All @@ -927,6 +962,25 @@ def _compute_layer_mask(self, layer, ds_slice, check_layers=False):

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]]
Expand Down Expand Up @@ -959,6 +1013,7 @@ def _generate_mask(self, *ds_slice, check_layers=False):
ds_slice, sub_slice = self._parse_ds_slice(ds_slice)

if self.layers:
print("Generating mask from layers")
force_include = []
for layer in self.layers:
if layer.force_include:
Expand Down
159 changes: 158 additions & 1 deletion tests/test_supply_curve_exclusions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@
"""
Exclusions unit test module
"""
import numpy as np
import os
import h5py
import shutil
import pytest
import warnings
import tempfile

import numpy as np

from reV import TESTDATADIR
from reV.handlers.exclusions import ExclusionLayers
Expand Down Expand Up @@ -167,6 +171,159 @@ def test_layer_mask(layer_name, inclusion_range, exclude_values,
assert np.allclose(truth, dict_test)



@pytest.mark.parametrize(('layer_name', 'inclusion_range', 'exclude_values',
'include_values', 'weight', 'exclude_nodata'), [
('ri_padus', (None, None), [1, ], None, 1, False),
('ri_padus', (None, None), [1, ], None, 1, True),
('ri_padus', (None, None), [1, ], None, 0.5, False),
('ri_padus', (None, None), [1, ], None, 0.5, True),
('ri_smod', (None, None), None, [1, ], 1, False),
('ri_smod', (None, None), None, [1, ], 1, True),
('ri_smod', (None, None), None, [1, ], 0.5, False),
('ri_srtm_slope', (None, 5), None, None, 1, False),
('ri_srtm_slope', (0, 5), None, None, 1, False),
('ri_srtm_slope', (0, 5), None, None, 1, True),
('ri_srtm_slope', (None, 5), None, None, 0.5, False),
('ri_srtm_slope', (None, 5), None, None, 0.5, True)])
@pytest.mark.parametrize("extent_type", ['iv', 'ev', 'ir', 'er'])
def test_layer_mask_with_extent(layer_name, inclusion_range, exclude_values,
include_values, weight, exclude_nodata,
extent_type):
"""
Test creation of layer masks with extent
Parameters
----------
layer_name : str
Layer name
inclusion_range : tuple
(min threshold, max threshold) for values to include
exclude_values : list
list of values to exclude
Note: Only supply exclusions OR inclusions
include_values : list
List of values to include
Note: Only supply inclusions OR exclusions
"""
extent = {"layer": "testing_regions"}
if extent_type == "iv":
extent["include_values"] = 0
elif extent_type == "ev":
extent["exclude_values"] = 1
elif extent_type == "ir":
extent["include_range"] = [0, 0]
elif extent_type == "er":
extent["exclude_range"] = [1, None]

excl_h5_og = os.path.join(TESTDATADIR, 'ri_exclusions', 'ri_exclusions.h5')
with tempfile.TemporaryDirectory() as td:
excl_fp = os.path.join(td, 'ri_exclusions.h5')
shutil.copy(excl_h5_og, excl_fp)

with ExclusionLayers(excl_fp) as f:
data = f[layer_name]
nodata_value = f.get_nodata_value(layer_name)

with h5py.File(excl_fp, mode="a") as fh:
halfway_x = data.shape[1] // 2
regions_bool = np.zeros(data.shape, dtype=np.float32)
regions_bool[:, halfway_x:] = 1
fh.create_dataset('testing_regions', data=regions_bool)

truth = mask_data(data, inclusion_range, exclude_values,
include_values, weight, exclude_nodata, nodata_value)
truth[:, halfway_x:] = 1

layer = LayerMask(layer_name, include_range=inclusion_range,
exclude_values=exclude_values,
include_values=include_values, weight=weight,
exclude_nodata=exclude_nodata,
nodata_value=nodata_value,
extent=extent)

mask_test = ExclusionMask.run(excl_fp, layers=layer)
assert np.allclose(truth, mask_test)

layer_dict = {layer_name: {"include_range": inclusion_range,
"exclude_values": exclude_values,
"include_values": include_values,
"weight": weight,
"exclude_nodata": exclude_nodata,
"extent": extent}}
dict_test = ExclusionMaskFromDict.run(excl_fp, layers_dict=layer_dict)
assert np.allclose(truth, dict_test)


def test_layer_mask_with_bad_extent():
"""
Test creation of layer masks with bad extent
Parameters
----------
layer_name : str
Layer name
inclusion_range : tuple
(min threshold, max threshold) for values to include
exclude_values : list
list of values to exclude
Note: Only supply exclusions OR inclusions
include_values : list
List of values to include
Note: Only supply inclusions OR exclusions
"""
layer_name = 'ri_padus'
inclusion_range = (None, None)
exclude_values = [1, ]
include_values = None
weight = 1
exclude_nodata = False

excl_h5_og = os.path.join(TESTDATADIR, 'ri_exclusions', 'ri_exclusions.h5')
with tempfile.TemporaryDirectory() as td:
excl_fp = os.path.join(td, 'ri_exclusions.h5')
shutil.copy(excl_h5_og, excl_fp)

with ExclusionLayers(excl_fp) as f:
data = f[layer_name]
nodata_value = f.get_nodata_value(layer_name)

with h5py.File(excl_fp, mode="a") as fh:
halfway_x = data.shape[1] // 2
regions_bool = np.zeros(data.shape, dtype=np.float32)
regions_bool[:, halfway_x:] = 1
fh.create_dataset('testing_regions', data=regions_bool)

regions_bool = np.zeros(data.shape, dtype=np.float32)
regions_bool[:, halfway_x:] = 2
fh.create_dataset('testing_regions_1', data=regions_bool)

layer = LayerMask(layer_name, include_range=inclusion_range,
exclude_values=exclude_values,
include_values=include_values, weight=weight,
exclude_nodata=exclude_nodata,
nodata_value=nodata_value,
extent={"layer": "testing_regions",
"include_values": 0,
"weight": 0.5})

with pytest.raises(ExclusionLayerError) as error:
ExclusionMask.run(excl_fp, layers=layer)

assert "Extent layer must be boolean" in str(error)

layer_dict = {layer_name: {"include_range": inclusion_range,
"exclude_values": exclude_values,
"include_values": include_values,
"weight": weight,
"exclude_nodata": exclude_nodata,
"extent": {"layer": "testing_regions_1",
"use_as_weights": True}}}
with pytest.raises(ExclusionLayerError) as error:
ExclusionMaskFromDict.run(excl_fp, layers_dict=layer_dict)
assert "Extent layer must be boolean" in str(error)


@pytest.mark.parametrize(('scenario'),
['urban_pv', 'rural_pv', 'wind', 'weighted'])
def test_inclusion_mask(scenario):
Expand Down

0 comments on commit 873af12

Please sign in to comment.