From 1995dade77f636d8cbce73e89abfd16ad1f78b26 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Mon, 22 Apr 2024 10:48:54 -0600 Subject: [PATCH 01/12] Update docstring --- reV/supply_curve/exclusions.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/reV/supply_curve/exclusions.py b/reV/supply_curve/exclusions.py index 4ec2b9429..63556f4ab 100644 --- a/reV/supply_curve/exclusions.py +++ b/reV/supply_curve/exclusions.py @@ -78,10 +78,12 @@ def __init__(self, layer, 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 From fa3c11dc5e47e67f2a699174a4ad8984f336994d Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Mon, 22 Apr 2024 12:31:04 -0600 Subject: [PATCH 02/12] update docstring --- reV/supply_curve/exclusions.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/reV/supply_curve/exclusions.py b/reV/supply_curve/exclusions.py index 63556f4ab..fd4a3bc2a 100644 --- a/reV/supply_curve/exclusions.py +++ b/reV/supply_curve/exclusions.py @@ -69,14 +69,16 @@ def __init__(self, layer, 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 - see 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). 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, From 96c3dcc42051663b4810880d2aed30b676f4b9e5 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Mon, 22 Apr 2024 12:31:30 -0600 Subject: [PATCH 03/12] Add error if no method is specified --- reV/supply_curve/exclusions.py | 10 ++++++++++ tests/test_supply_curve_exclusions.py | 13 +++++++++++++ 2 files changed, 23 insertions(+) diff --git a/reV/supply_curve/exclusions.py b/reV/supply_curve/exclusions.py index fd4a3bc2a..c2079f690 100644 --- a/reV/supply_curve/exclusions.py +++ b/reV/supply_curve/exclusions.py @@ -359,6 +359,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 " diff --git a/tests/test_supply_curve_exclusions.py b/tests/test_supply_curve_exclusions.py index eae2da3b3..54c604d94 100644 --- a/tests/test_supply_curve_exclusions.py +++ b/tests/test_supply_curve_exclusions.py @@ -92,6 +92,19 @@ def mask_data(data, inclusion_range, exclude_values, include_values, return mask +def test_error_for_empty_exclusions(): + """Test error is thrown for empty layer specification. """ + + with pytest.raises(ExclusionLayerError) as error: + LayerMask("test") + + expected_msg = ( + "Exactly one approach must be specified to create the inclusion " + "mask for layer 'test'!" + ) + assert expected_msg in str(error) + + @pytest.mark.parametrize(('layer_name', 'inclusion_range', 'exclude_values', 'include_values', 'weight', 'exclude_nodata'), [ ('ri_padus', (None, None), [1, ], None, 1, False), From 6a65f985ac35ec9f176916a4ac15102efc79257b Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Mon, 22 Apr 2024 14:03:04 -0600 Subject: [PATCH 04/12] Refactor mask generation logic --- reV/supply_curve/exclusions.py | 80 ++++++++++++++-------------------- 1 file changed, 33 insertions(+), 47 deletions(-) diff --git a/reV/supply_curve/exclusions.py b/reV/supply_curve/exclusions.py index c2079f690..dfaba7e38 100644 --- a/reV/supply_curve/exclusions.py +++ b/reV/supply_curve/exclusions.py @@ -902,34 +902,34 @@ 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) + + 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 _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): """ @@ -964,27 +964,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, From 6d4de77db91d661d31776fb5973b43e5ddaf9a1c Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Mon, 22 Apr 2024 16:29:53 -0600 Subject: [PATCH 05/12] No error if layer used as weights --- reV/supply_curve/exclusions.py | 2 +- tests/test_supply_curve_exclusions.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/reV/supply_curve/exclusions.py b/reV/supply_curve/exclusions.py index dfaba7e38..e71a21b79 100644 --- a/reV/supply_curve/exclusions.py +++ b/reV/supply_curve/exclusions.py @@ -359,7 +359,7 @@ def _check_mask_type(self): logger.error(msg) raise ExclusionLayerError(msg) - if mask is None: + if mask is None and not self._as_weights: 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`, ' diff --git a/tests/test_supply_curve_exclusions.py b/tests/test_supply_curve_exclusions.py index 54c604d94..c27cd1693 100644 --- a/tests/test_supply_curve_exclusions.py +++ b/tests/test_supply_curve_exclusions.py @@ -104,6 +104,8 @@ def test_error_for_empty_exclusions(): ) assert expected_msg in str(error) + __ = LayerMask("test", use_as_weights=True) # no error + @pytest.mark.parametrize(('layer_name', 'inclusion_range', 'exclude_values', 'include_values', 'weight', 'exclude_nodata'), [ From 873af122910a50590995a11fece7ea021731a8b0 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Mon, 22 Apr 2024 16:31:31 -0600 Subject: [PATCH 06/12] Add `extent` option --- reV/supply_curve/exclusions.py | 93 ++++++++++++--- tests/test_supply_curve_exclusions.py | 159 +++++++++++++++++++++++++- 2 files changed, 232 insertions(+), 20 deletions(-) diff --git a/reV/supply_curve/exclusions.py b/reV/supply_curve/exclusions.py index e71a21b79..9fcb70238 100644 --- a/reV/supply_curve/exclusions.py +++ b/reV/supply_curve/exclusions.py @@ -32,6 +32,7 @@ def __init__(self, layer, weight=1.0, exclude_nodata=False, nodata_value=None, + extent=None, **kwargs): """ Parameters @@ -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, @@ -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_*``. @@ -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 "{}"' @@ -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}.' @@ -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]] @@ -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: diff --git a/tests/test_supply_curve_exclusions.py b/tests/test_supply_curve_exclusions.py index c27cd1693..b1ea568db 100644 --- a/tests/test_supply_curve_exclusions.py +++ b/tests/test_supply_curve_exclusions.py @@ -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 @@ -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): From 11f2eaf5f58009b6f869c6c731ad0d587700414d Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Mon, 22 Apr 2024 16:42:35 -0600 Subject: [PATCH 07/12] Documentation updates --- reV/supply_curve/exclusions.py | 13 +++++++------ reV/supply_curve/sc_aggregation.py | 7 +++++++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/reV/supply_curve/exclusions.py b/reV/supply_curve/exclusions.py index 9fcb70238..1129f2887 100644 --- a/reV/supply_curve/exclusions.py +++ b/reV/supply_curve/exclusions.py @@ -129,12 +129,13 @@ def __init__(self, layer, 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. + 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_*``. diff --git a/reV/supply_curve/sc_aggregation.py b/reV/supply_curve/sc_aggregation.py index 387382dac..6621205a1 100644 --- a/reV/supply_curve/sc_aggregation.py +++ b/reV/supply_curve/sc_aggregation.py @@ -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] + } + } ... } From 686f892bb29999f6f3ccf6562828fec4b921dbbb Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Mon, 22 Apr 2024 16:45:37 -0600 Subject: [PATCH 08/12] Linter fix --- tests/test_supply_curve_exclusions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_supply_curve_exclusions.py b/tests/test_supply_curve_exclusions.py index b1ea568db..90bf75a2b 100644 --- a/tests/test_supply_curve_exclusions.py +++ b/tests/test_supply_curve_exclusions.py @@ -171,7 +171,6 @@ 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), From f68c00556baa795cd290339280ca7ac11116033b Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Mon, 22 Apr 2024 17:30:24 -0600 Subject: [PATCH 09/12] Slightly adjust logic --- reV/supply_curve/exclusions.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/reV/supply_curve/exclusions.py b/reV/supply_curve/exclusions.py index 1129f2887..9991034df 100644 --- a/reV/supply_curve/exclusions.py +++ b/reV/supply_curve/exclusions.py @@ -394,15 +394,15 @@ def _check_mask_type(self): logger.error(msg) raise ExclusionLayerError(msg) - if mask is None and not self._as_weights: - 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 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 " From d5c7d8026716ffd03dacb85c3df919a1d39bc79a Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Mon, 22 Apr 2024 17:30:43 -0600 Subject: [PATCH 10/12] Add `use_as_weights` test --- tests/test_supply_curve_exclusions.py | 65 +++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/tests/test_supply_curve_exclusions.py b/tests/test_supply_curve_exclusions.py index 90bf75a2b..5a48f9738 100644 --- a/tests/test_supply_curve_exclusions.py +++ b/tests/test_supply_curve_exclusions.py @@ -323,6 +323,71 @@ def test_layer_mask_with_bad_extent(): assert "Extent layer must be boolean" in str(error) +def test_layer_mask_with_extent_as_weights(): + """ + 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) + + data[:, 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, + use_as_weights=True, + extent={"layer": "testing_regions", + "include_values": 0}) + + mask_test = ExclusionMask.run(excl_fp, layers=layer) + assert np.allclose(data, mask_test) + + layer_dict = {layer_name: {"include_range": inclusion_range, + "exclude_values": exclude_values, + "include_values": include_values, + "weight": weight, + "exclude_nodata": exclude_nodata, + "use_as_weights": True, + "extent": {"layer": "testing_regions", + "include_values": 0}}} + dict_test = ExclusionMaskFromDict.run(excl_fp, layers_dict=layer_dict) + assert np.allclose(data, dict_test) + + @pytest.mark.parametrize(('scenario'), ['urban_pv', 'rural_pv', 'wind', 'weighted']) def test_inclusion_mask(scenario): From 6c938d9e8d61aa35513e9c71deeb5840e1a98f6d Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Fri, 26 Apr 2024 09:05:32 -0600 Subject: [PATCH 11/12] Bump version --- reV/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reV/version.py b/reV/version.py index 06d3b84ec..756b3ea19 100644 --- a/reV/version.py +++ b/reV/version.py @@ -2,4 +2,4 @@ reV Version number """ -__version__ = "0.8.7" +__version__ = "0.8.8" From 9252c3dac53733864139f25b2fefdf14222feb31 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Mon, 29 Apr 2024 16:34:29 -0600 Subject: [PATCH 12/12] Formatting --- reV/supply_curve/exclusions.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/reV/supply_curve/exclusions.py b/reV/supply_curve/exclusions.py index 9991034df..2342998ac 100644 --- a/reV/supply_curve/exclusions.py +++ b/reV/supply_curve/exclusions.py @@ -396,11 +396,11 @@ def _check_mask_type(self): 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)) + '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) @@ -1014,7 +1014,6 @@ 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: