Repository: natcap/invest (python)

Issue:
Coastal Blue Carbon error with just one snapshot year
A [user reported](https://community.naturalcapitalproject.org/t/i-have-error-in-run-coastal-blue-carbon-model/4501) an error in CBC:
```
File “natcap\invest\coastal_blue_carbon\coastal_blue_carbon.py”, line 956, in execute
KeyError: ‘soil’
```
The model is expecting there to be more than one snapshot year, ultimately causing this KeyError. A possible workaround is to set the Analysis Year to the snapshot year + 1, but that shouldn't be necessary. @phargogh confirmed over slack that CBC should work with just one snapshot year, though results will be very limited.

Pull request description:
stricter validation for CBC analysis year
## Description
Fixes #1534

## Checklist
- [x] Updated HISTORY.rst and link to any relevant issue (if these changes are user-facing)
- [x] Updated the user's guide (if needed)
- [ ] Tested the Workbench UI (if relevant)


Patch:
diff --git a/HISTORY.rst b/HISTORY.rst
index ed6a057ed..5b525106a 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -68,6 +68,13 @@ Unreleased Changes
 * Annual Water Yield
     * Added the results_suffix to a few intermediate files where it was
       missing. https://github.com/natcap/invest/issues/1517
+* Coastal Blue Carbon
+    * Updated model validation to prevent the case where a user provides only
+      one snapshot year and no analysis year
+      (`#1534 <https://github.com/natcap/invest/issues/1534>`_).
+      Also enforces that the analysis year, if provided, is greater than the
+      latest snapshot year. An analysis year equal to the latest snapshot year
+      is no longer allowed.
 * Coastal Vulnerability
     * Fixed a bug in handling ``nan`` as the nodata value of the bathymetry
       raster. ``nan`` pixels will now be propertly ignored before calculating
diff --git a/Makefile b/Makefile
index c0171ce77..2283ed914 100644
--- a/Makefile
+++ b/Makefile
@@ -2,7 +2,7 @@
 DATA_DIR := data
 GIT_SAMPLE_DATA_REPO        := https://bitbucket.org/natcap/invest-sample-data.git
 GIT_SAMPLE_DATA_REPO_PATH   := $(DATA_DIR)/invest-sample-data
-GIT_SAMPLE_DATA_REPO_REV    := 2e7cd618c661ec3f3b2a3bddfd2ce7d4704abc05
+GIT_SAMPLE_DATA_REPO_REV    := 8f78a4873f1fa4253d3ab9a01d6e23f11499b975
 
 GIT_TEST_DATA_REPO          := https://bitbucket.org/natcap/invest-test-data.git
 GIT_TEST_DATA_REPO_PATH     := $(DATA_DIR)/invest-test-data
@@ -10,7 +10,7 @@ GIT_TEST_DATA_REPO_REV      := 324abde73e1d770ad75921466ecafd1ec6297752
 
 GIT_UG_REPO                 := https://github.com/natcap/invest.users-guide
 GIT_UG_REPO_PATH            := doc/users-guide
-GIT_UG_REPO_REV             := fa6b181d49136089dce56d4ff8f3dcaf12eb4ced
+GIT_UG_REPO_REV             := 0404bc5d4d43085cdc58f50f8fc29944b10cefb1
 
 ENV = "./env"
 ifeq ($(OS),Windows_NT)
diff --git a/src/natcap/invest/coastal_blue_carbon/coastal_blue_carbon.py b/src/natcap/invest/coastal_blue_carbon/coastal_blue_carbon.py
index e322336ba..7e118cdd5 100644
--- a/src/natcap/invest/coastal_blue_carbon/coastal_blue_carbon.py
+++ b/src/natcap/invest/coastal_blue_carbon/coastal_blue_carbon.py
@@ -114,8 +114,10 @@ from .. import gettext
 LOGGER = logging.getLogger(__name__)
 
 INVALID_ANALYSIS_YEAR_MSG = gettext(
-    "Analysis year {analysis_year} must be >= the latest snapshot year "
+    "Analysis year ({analysis_year}) must be greater than the latest snapshot year "
     "({latest_year})")
+MISSING_ANALYSIS_YEAR_MSG = gettext(
+    "Analysis year is required if only one snapshot year is provided.")
 INVALID_TRANSITION_VALUES_MSG = gettext(
     "The transition table expects values of {model_transitions} but found "
     "values of {transition_values}.")
@@ -2174,9 +2176,14 @@ def validate(args, limit_to=None):
             **MODEL_SPEC['args']['landcover_snapshot_csv']
         )['raster_path'].to_dict()
 
+        snapshot_years = set(snapshots.keys())
+        if len(snapshot_years) == 1 and "analysis_year" not in sufficient_keys:
+            validation_warnings.append(
+                (['analysis_year'], MISSING_ANALYSIS_YEAR_MSG))
+
         if ("analysis_year" not in invalid_keys
                 and "analysis_year" in sufficient_keys):
-            if max(set(snapshots.keys())) > int(args['analysis_year']):
+            if max(snapshot_years) >= int(args['analysis_year']):
                 validation_warnings.append((
                     ['analysis_year'],
                     INVALID_ANALYSIS_YEAR_MSG.format(


Test patch:
diff --git a/tests/test_coastal_blue_carbon.py b/tests/test_coastal_blue_carbon.py
index 785181e78..10c983144 100644
--- a/tests/test_coastal_blue_carbon.py
+++ b/tests/test_coastal_blue_carbon.py
@@ -1034,3 +1034,28 @@ class TestCBC2(unittest.TestCase):
                 expected_year_of_disturbance)
         finally:
             raster = None
+
+    def test_validate_required_analysis_year(self):
+        """CBC: analysis year validation (regression test for #1534)."""
+        from natcap.invest.coastal_blue_carbon import coastal_blue_carbon
+
+        args = TestCBC2._create_model_args(self.workspace_dir)
+        args['workspace_dir'] = self.workspace_dir
+        args['analysis_year'] = None
+        # truncate the CSV so that it has only one snapshot year
+        with open(args['landcover_snapshot_csv'], 'r') as file:
+            lines = file.readlines()
+        with open(args['landcover_snapshot_csv'], 'w') as file:
+            file.writelines(lines[:2])
+        validation_warnings = coastal_blue_carbon.validate(args)
+        self.assertEqual(
+            validation_warnings,
+            [(['analysis_year'], coastal_blue_carbon.MISSING_ANALYSIS_YEAR_MSG)])
+
+        args['analysis_year'] = 2000  # set analysis year equal to snapshot year
+        validation_warnings = coastal_blue_carbon.validate(args)
+        self.assertEqual(
+            validation_warnings,
+            [(['analysis_year'],
+                coastal_blue_carbon.INVALID_ANALYSIS_YEAR_MSG.format(
+                    analysis_year=2000, latest_year=2000))])