Skip to content

Commit 0b35f58

Browse files
authored
Merge pull request natcap#1649 from emilyanndavis/enhancement/721-hq-rarity-csv
HQ: generate rarity output in CSV format (in addition to raster)
2 parents 62c808b + c56523b commit 0b35f58

File tree

3 files changed

+136
-18
lines changed

3 files changed

+136
-18
lines changed

HISTORY.rst

+2
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ Unreleased Changes
6767
* Habitat Quality
6868
* Access raster is now generated from the reprojected access vector
6969
(https://github.com/natcap/invest/issues/1615).
70+
* Rarity values are now output in CSV format (as well as in raster format)
71+
(https://github.com/natcap/invest/issues/721).
7072
* Urban Flood Risk
7173
* Fields present on the input AOI vector are now retained in the output.
7274
(https://github.com/natcap/invest/issues/1600)

src/natcap/invest/habitat_quality.py

+106-17
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# coding=UTF-8
22
"""InVEST Habitat Quality model."""
33
import collections
4+
import csv
45
import logging
56
import os
67

@@ -213,36 +214,36 @@
213214
"output": {
214215
"type": "directory",
215216
"contents": {
216-
"deg_sum_out_c.tif": {
217+
"deg_sum_c.tif": {
217218
"about": (
218219
"Relative level of habitat degradation on the current "
219220
"landscape."),
220221
"bands": {1: {"type": "ratio"}}
221222
},
222-
"deg_sum_out_f.tif": {
223+
"deg_sum_f.tif": {
223224
"about": (
224225
"Relative level of habitat degradation on the future "
225226
"landscape."),
226227
"bands": {1: {"type": "ratio"}},
227228
"created_if": "lulc_fut_path"
228229
},
229-
"quality_out_c.tif": {
230+
"quality_c.tif": {
230231
"about": (
231232
"Relative level of habitat quality on the current "
232233
"landscape."),
233234
"bands": {1: {"type": "ratio"}}
234235
},
235-
"quality_out_f.tif": {
236+
"quality_f.tif": {
236237
"about": (
237238
"Relative level of habitat quality on the future "
238239
"landscape."),
239240
"bands": {1: {"type": "ratio"}},
240241
"created_if": "lulc_fut_path"
241242
},
242-
"rarity_out_c.tif": {
243+
"rarity_c.tif": {
243244
"about": (
244245
"Relative habitat rarity on the current landscape "
245-
"vis-a-vis the baseline map. The grid cells values "
246+
"vis-a-vis the baseline map. The grid cell's values "
246247
"are defined between a range of 0 and 1 where 0.5 "
247248
"indicates no abundance change between the baseline "
248249
"and current or projected map. Values between 0 and 0.5 "
@@ -258,10 +259,10 @@
258259
"created_if": "lulc_bas_path",
259260
"bands": {1: {"type": "ratio"}}
260261
},
261-
"rarity_out_f.tif": {
262+
"rarity_f.tif": {
262263
"about": (
263264
"Relative habitat rarity on the future landscape "
264-
"vis-a-vis the baseline map. The grid cells values "
265+
"vis-a-vis the baseline map. The grid cell's values "
265266
"are defined between a range of 0 and 1 where 0.5 "
266267
"indicates no abundance change between the baseline "
267268
"and current or projected map. Values between 0 and "
@@ -278,6 +279,70 @@
278279
"created_if": "lulc_bas_path and lulc_fut_path",
279280
"bands": {1: {"type": "ratio"}}
280281
},
282+
"rarity_c.csv": {
283+
"about": ("Table of rarity values by LULC code for the "
284+
"current landscape."),
285+
"index_col": "lulc_code",
286+
"columns": {
287+
"lulc_code": {
288+
"type": "number",
289+
"units": u.none,
290+
"about": "LULC class",
291+
},
292+
"rarity_value": {
293+
"type": "number",
294+
"units": u.none,
295+
"about": (
296+
"Relative habitat rarity on the current landscape "
297+
"vis-a-vis the baseline map. The rarity values "
298+
"are defined between a range of 0 and 1 where 0.5 "
299+
"indicates no abundance change between the baseline "
300+
"and current or projected map. Values between 0 and 0.5 "
301+
"indicate a habitat is more abundant and the closer "
302+
"the value is to 0 the lesser the likelihood that the "
303+
"preservation of that habitat type on the current or "
304+
"future landscape is important to biodiversity conservation. "
305+
"Values between 0.5 and 1 indicate a habitat is less "
306+
"abundant and the closer the value is to 1 the greater "
307+
"the likelihood that the preservation of that habitat "
308+
"type on the current or future landscape is important "
309+
"to biodiversity conservation."),
310+
},
311+
},
312+
"created_if": "lulc_bas_path",
313+
},
314+
"rarity_f.csv": {
315+
"about": ("Table of rarity values by LULC code for the "
316+
"future landscape."),
317+
"index_col": "lulc_code",
318+
"columns": {
319+
"lulc_code": {
320+
"type": "number",
321+
"units": u.none,
322+
"about": "LULC class",
323+
},
324+
"rarity_value": {
325+
"type": "number",
326+
"units": u.none,
327+
"about": (
328+
"Relative habitat rarity on the future landscape "
329+
"vis-a-vis the baseline map. The rarity values "
330+
"are defined between a range of 0 and 1 where 0.5 "
331+
"indicates no abundance change between the baseline "
332+
"and current or projected map. Values between 0 and 0.5 "
333+
"indicate a habitat is more abundant and the closer "
334+
"the value is to 0 the lesser the likelihood that the "
335+
"preservation of that habitat type on the current or "
336+
"future landscape is important to biodiversity conservation. "
337+
"Values between 0.5 and 1 indicate a habitat is less "
338+
"abundant and the closer the value is to 1 the greater "
339+
"the likelihood that the preservation of that habitat "
340+
"type on the current or future landscape is important "
341+
"to biodiversity conservation."),
342+
},
343+
},
344+
"created_if": "lulc_bas_path and lulc_fut_path",
345+
},
281346
}
282347
},
283348
"intermediate": {
@@ -743,13 +808,16 @@ def execute(args):
743808
intermediate_output_dir,
744809
f'new_cover{lulc_key}{file_suffix}.tif')
745810

746-
rarity_path = os.path.join(
811+
rarity_raster_path = os.path.join(
747812
output_dir, f'rarity{lulc_key}{file_suffix}.tif')
748813

814+
rarity_csv_path = os.path.join(
815+
output_dir, f'rarity{lulc_key}{file_suffix}.csv')
816+
749817
_ = task_graph.add_task(
750818
func=_compute_rarity_operation,
751819
args=((lulc_base_path, 1), (lulc_path, 1), (new_cover_path, 1),
752-
rarity_path),
820+
rarity_raster_path, rarity_csv_path),
753821
dependent_task_list=[align_task],
754822
task_name=f'rarity{lulc_time}')
755823

@@ -773,7 +841,7 @@ def _calculate_habitat_quality(deg_hab_raster_list, quality_out_path, ksq):
773841
pygeoprocessing.raster_map(
774842
op=lambda degradation, habitat: (
775843
habitat * (1 - (degradation**_SCALING_PARAM) /
776-
(degradation**_SCALING_PARAM + ksq))),
844+
(degradation**_SCALING_PARAM + ksq))),
777845
rasters=deg_hab_raster_list,
778846
target_path=quality_out_path)
779847

@@ -829,8 +897,9 @@ def total_degradation(*arrays):
829897

830898

831899
def _compute_rarity_operation(
832-
base_lulc_path_band, lulc_path_band, new_cover_path, rarity_path):
833-
"""Calculate habitat rarity.
900+
base_lulc_path_band, lulc_path_band, new_cover_path,
901+
rarity_raster_path, rarity_csv_path):
902+
"""Calculate habitat rarity and generate raster and CSV output.
834903
835904
Output rarity values will be an index from 0 - 1 where:
836905
pixel > 0.5 - more rare
@@ -846,7 +915,8 @@ def _compute_rarity_operation(
846915
new_cover_path (tuple): a 2 tuple for the path to intermediate
847916
raster file for trimming ``lulc_path_band`` to
848917
``base_lulc_path_band`` of the form (path, band index).
849-
rarity_path (string): path to output rarity raster.
918+
rarity_raster_path (string): path to output rarity raster.
919+
rarity_csv_path (string): path to output rarity CSV.
850920
851921
Returns:
852922
None
@@ -857,15 +927,13 @@ def _compute_rarity_operation(
857927
base_lulc_path_band[0])
858928
base_pixel_size = base_raster_info['pixel_size']
859929
base_area = float(abs(base_pixel_size[0]) * abs(base_pixel_size[1]))
860-
base_nodata = base_raster_info['nodata'][0]
861930

862931
lulc_code_count_b = _raster_pixel_count(base_lulc_path_band)
863932

864933
# get the area of a cur/fut pixel
865934
lulc_raster_info = pygeoprocessing.get_raster_info(lulc_path_band[0])
866935
lulc_pixel_size = lulc_raster_info['pixel_size']
867936
lulc_area = float(abs(lulc_pixel_size[0]) * abs(lulc_pixel_size[1]))
868-
lulc_nodata = lulc_raster_info['nodata'][0]
869937

870938
# Trim cover_x to the mask of base.
871939
pygeoprocessing.raster_map(
@@ -895,13 +963,34 @@ def _compute_rarity_operation(
895963
code_index[code] = 0.0
896964

897965
pygeoprocessing.reclassify_raster(
898-
new_cover_path, code_index, rarity_path, gdal.GDT_Float32,
966+
new_cover_path, code_index, rarity_raster_path, gdal.GDT_Float32,
899967
_OUT_NODATA)
900968

969+
_generate_rarity_csv(code_index, rarity_csv_path)
970+
901971
LOGGER.info('Finished rarity computation on'
902972
f' {os.path.basename(lulc_path_band[0])} land cover.')
903973

904974

975+
def _generate_rarity_csv(rarity_dict, target_csv_path):
976+
"""Generate CSV containing rarity values by LULC code.
977+
978+
Args:
979+
rarity_dict (dict): dictionary containing LULC codes (as keys)
980+
and their associated rarity values (as values).
981+
target_csv_path (string): path to output CSV.
982+
983+
Returns:
984+
None
985+
"""
986+
lulc_codes = sorted(rarity_dict)
987+
with open(target_csv_path, 'w', newline='') as csvfile:
988+
writer = csv.writer(csvfile, delimiter=',')
989+
writer.writerow(['lulc_code', 'rarity_value'])
990+
for lulc_code in lulc_codes:
991+
writer.writerow([lulc_code, rarity_dict[lulc_code]])
992+
993+
905994
def _raster_pixel_count(raster_path_band):
906995
"""Count unique pixel values in single band raster.
907996

tests/test_habitat_quality.py

+28-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Module for Regression Testing the InVEST Habitat Quality model."""
2+
import csv
23
import os
34
import shutil
45
import tempfile
@@ -245,6 +246,33 @@ def test_habitat_quality_presence_absence_regression(self):
245246
# so we should exclude those new nodata pixel values.
246247
assert_array_sum(raster_path, assert_value, include_nodata=False)
247248

249+
# Based on the scenarios used to generate the rasters above,
250+
# rarity values are calculated as follows:
251+
# For LULC 1, rarity = 1.0 - (5000 / (10000 + 5000)) = 0.6667.
252+
# For LULC 2 and 3, rarity = 0.0 because they are not in the baseline.
253+
expected_csv_values = {
254+
'rarity_c_regression.csv': [
255+
(1, 0.6667, 4),
256+
(2, 0.0, 0),
257+
],
258+
'rarity_f_regression.csv': [
259+
(1, 0.6667, 4),
260+
(3, 0.0, 0),
261+
],
262+
}
263+
for csv_filename in expected_csv_values.keys():
264+
csv_path = os.path.join(args['workspace_dir'], csv_filename)
265+
with open(csv_path, newline='') as csvfile:
266+
reader = csv.DictReader(csvfile, delimiter=',')
267+
self.assertEqual(reader.fieldnames,
268+
['lulc_code', 'rarity_value'])
269+
for (exp_lulc, exp_rarity,
270+
places_to_round) in expected_csv_values[csv_filename]:
271+
row = next(reader)
272+
self.assertEqual(int(row['lulc_code']), exp_lulc)
273+
self.assertAlmostEqual(float(row['rarity_value']),
274+
exp_rarity, places_to_round)
275+
248276
def test_habitat_quality_regression_different_projections(self):
249277
"""Habitat Quality: base regression test with simplified data."""
250278
from natcap.invest import habitat_quality
@@ -1047,7 +1075,6 @@ def test_habitat_quality_case_insensitivty(self):
10471075
habitat_quality.execute(args)
10481076

10491077
# Reasonable to just check quality out in this case
1050-
#assert_array_sum(
10511078
assert_array_sum(
10521079
os.path.join(args['workspace_dir'], 'quality_c.tif'),
10531080
5852.088)

0 commit comments

Comments
 (0)