From fe2bd098a6589613949840ebf38142bfc3a235e9 Mon Sep 17 00:00:00 2001 From: Matthew Harris Date: Mon, 21 Apr 2025 15:59:44 +0100 Subject: [PATCH 01/12] Update redlist key param name --- ecoscape_layers/redlist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecoscape_layers/redlist.py b/ecoscape_layers/redlist.py index 0e753b5..1207088 100644 --- a/ecoscape_layers/redlist.py +++ b/ecoscape_layers/redlist.py @@ -13,7 +13,7 @@ def __init__(self, redlist_key: str, ebird_key: str | None = None): Initializes a RedList object. API keys are required to access the IUCN Red List API and eBird API respectively; see the documentation for more information. """ - self.redlist_params = {"token": redlist_key} + self.redlist_params = {"Authorization": redlist_key} self.ebird_key = ebird_key def get_from_redlist(self, url: str) -> dict: From 24842002f50d5ed09a09ccb1c7f95e88ca617d61 Mon Sep 17 00:00:00 2001 From: Matthew Harris Date: Mon, 21 Apr 2025 15:59:58 +0100 Subject: [PATCH 02/12] Redlist key now header not token --- ecoscape_layers/redlist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecoscape_layers/redlist.py b/ecoscape_layers/redlist.py index 1207088..6216f60 100644 --- a/ecoscape_layers/redlist.py +++ b/ecoscape_layers/redlist.py @@ -23,7 +23,7 @@ def get_from_redlist(self, url: str) -> dict: :param url: the URL for the request. :return: response for the request. """ - res = requests.get(url, params=self.redlist_params) + res = requests.get(url, headers=self.redlist_params) if res.status_code != 200: raise ValueError(f"Error {res.status_code} in Red List API request") From e811b441aa1a84305a432a3fcbfd3939f988f6ae Mon Sep 17 00:00:00 2001 From: Matthew Harris Date: Mon, 21 Apr 2025 16:00:31 +0100 Subject: [PATCH 03/12] Get habitat info with new API --- ecoscape_layers/redlist.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/ecoscape_layers/redlist.py b/ecoscape_layers/redlist.py index 6216f60..bbf143a 100644 --- a/ecoscape_layers/redlist.py +++ b/ecoscape_layers/redlist.py @@ -95,12 +95,23 @@ def get_habitat_data( sci_name = self.get_scientific_name(species_name) else: sci_name = species_name - - url = f"https://apiv3.iucnredlist.org/api/v3/habitats/species/name/{sci_name}" - if region is not None: - url += f"/region/{region}" - - habs = self.get_from_redlist(url) + # Split sci_name into genus and species + genus, species = sci_name.split() + + url = f"https://api.iucnredlist.org/api/v4/taxa/scientific_name?genus_name={genus}&species_name={species}" + + # if region is not None: + # url += f"/region/{region}" + assessments = self.get_from_redlist(url)["assessments"] + + # Get assessment code for latest global assessment for species + latest_assessment = [i for i in assessments if ((i["latest"] == True) & (i["scopes"][0]["code"] == '1'))][0]["assessment_id"] + + # Get habitats from latest global assessment for species + url = f"https://api.iucnredlist.org/api/v4/assessment/{latest_assessment}" + + habs = self.get_from_redlist(url)["habitats"] + res = {} for hab in habs: From 56dee6d7521a5effbc767c1579051346cfa96533 Mon Sep 17 00:00:00 2001 From: Matthew Harris Date: Mon, 21 Apr 2025 16:00:44 +0100 Subject: [PATCH 04/12] Hab code sep now _ --- ecoscape_layers/redlist.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ecoscape_layers/redlist.py b/ecoscape_layers/redlist.py index bbf143a..86ab5a2 100644 --- a/ecoscape_layers/redlist.py +++ b/ecoscape_layers/redlist.py @@ -117,9 +117,10 @@ def get_habitat_data( for hab in habs: code = str(hab["code"]) + # TODO: is this necessary? Codes seem to be x_x instea of xx.xx # some codes are in the format xx.xx.xx instead of xx.xx # we will truncate xx.xx.xx codes to xx.xx - code_sep = code.split(".") + code_sep = code.split("_") # check that code_sep len is not less than len of 2 if len(code_sep) < 2: From 9d25fbc8e7d3aff0e7cb8b178cc89c4f85ae0a77 Mon Sep 17 00:00:00 2001 From: Matthew Harris Date: Mon, 21 Apr 2025 16:00:51 +0100 Subject: [PATCH 05/12] Fix key name --- ecoscape_layers/redlist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecoscape_layers/redlist.py b/ecoscape_layers/redlist.py index 86ab5a2..809f148 100644 --- a/ecoscape_layers/redlist.py +++ b/ecoscape_layers/redlist.py @@ -137,7 +137,7 @@ def get_habitat_data( code_sep = map(lambda num_str: num_str.zfill(2), code_sep) # Convert bool like strings to bools - hab["majorimportance"] = hab["majorimportance"] == "Yes" + hab["majorImportance"] = hab["majorImportance"] == "Yes" hab["suitability"] = hab["suitability"] == "Suitable" # create a map_code that is represented by an int From b611ab47321c9491515dc051c7ab6647f9284774 Mon Sep 17 00:00:00 2001 From: Matthew Harris Date: Mon, 21 Apr 2025 16:06:58 +0100 Subject: [PATCH 06/12] Fix return from get_from_redlist --- ecoscape_layers/redlist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecoscape_layers/redlist.py b/ecoscape_layers/redlist.py index 809f148..67d8cc5 100644 --- a/ecoscape_layers/redlist.py +++ b/ecoscape_layers/redlist.py @@ -30,7 +30,7 @@ def get_from_redlist(self, url: str) -> dict: data: dict = res.json() - return data["result"] + return data def get_scientific_name(self, species_code: str) -> str: """Translates eBird codes to scientific names for use in Red List. From 502f3bbbbccfc27d1d05245cbfad5c08af557399 Mon Sep 17 00:00:00 2001 From: Matthew Harris Date: Thu, 24 Apr 2025 10:32:08 +0100 Subject: [PATCH 07/12] Update hab key majorimportance -> majorImportance --- ecoscape_layers/utils.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ecoscape_layers/utils.py b/ecoscape_layers/utils.py index 96e0fc4..d75fc86 100644 --- a/ecoscape_layers/utils.py +++ b/ecoscape_layers/utils.py @@ -228,7 +228,7 @@ def default_refinement_method( if map_code not in habitats: return 1.0 - if habitats[map_code]["majorimportance"]: + if habitats[map_code]["majorImportance"]: return 0.0 elif habitats[map_code]["suitability"]: return 0.1 @@ -326,7 +326,7 @@ def get_current_habitat( Inputs for the overrides may consist of integers that represent specific map codes and strings that represent keywords, which will then be converted into map codes. These keywords can take the form of the IUCN Habitat Classification Scheme categories listed in the constants.py file. Additionally, you can - specify "majorimportance" or "suitable" to only use habitats with these qualities for a species. + specify "majorImportance" or "suitable" to only use habitats with these qualities for a species. Examples: overrides_forest308: ["forest", 308] @@ -352,19 +352,19 @@ def get_current_habitat( # replace all IUCN habitat classification scheme keywords with map codes overrides = iucn_habs_to_codes(overrides) - # replace keywords (majorimportance, suitable) with map codes + # replace keywords (majorImportance, suitable) with map codes # search for the keywords and error on invalid keywords major_i = None suit_i = None for i in range(len(overrides)): - if overrides[i] == "majorimportance": + if overrides[i] == "majorImportance": major_i = i elif overrides[i] == "suitable": suit_i = i elif type(overrides[i]) is str: error = f"""\ Keyword {overrides[i]} not found in IUCN habitat classification scheme keywords - and is not 'majorimportance' or 'suitable'.""" + and is not 'majorImportance' or 'suitable'.""" raise KeyError(dedent(error)) # define function to add new codes based on keyword @@ -390,7 +390,7 @@ def replace_keywords(keyword: str, keyword_i: int): # add new codes based on keywords if major_i is not None: - replace_keywords("majorimportance", major_i) + replace_keywords("majorImportance", major_i) if suit_i is not None: replace_keywords("suitable", suit_i) @@ -408,7 +408,7 @@ def replace_keywords(keyword: str, keyword_i: int): # The default action is to return map_codes in which habitat is considered major importance by IUCN output = [] for code, hab in habitats.items(): - if hab["majorimportance"]: + if hab["majorImportance"]: output.append(code) return output From 9de8daa71015cb6276ffec5983b18231ca387d17 Mon Sep 17 00:00:00 2001 From: Matthew Harris Date: Thu, 24 Apr 2025 10:32:40 +0100 Subject: [PATCH 08/12] Update hab key suitable -> suitability --- ecoscape_layers/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecoscape_layers/utils.py b/ecoscape_layers/utils.py index d75fc86..1d76d8c 100644 --- a/ecoscape_layers/utils.py +++ b/ecoscape_layers/utils.py @@ -392,7 +392,7 @@ def replace_keywords(keyword: str, keyword_i: int): if major_i is not None: replace_keywords("majorImportance", major_i) if suit_i is not None: - replace_keywords("suitable", suit_i) + replace_keywords("suitability", suit_i) # remove and duplicates due to overlap overrides = list(set(overrides)) From 6019a90243b293bea87037006a0c4e8267531a8b Mon Sep 17 00:00:00 2001 From: Matthew Harris Date: Thu, 24 Apr 2025 10:32:55 +0100 Subject: [PATCH 09/12] Add key directory (private) --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 2937d4e..c9af326 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ tests/config.py .venv venv + +private \ No newline at end of file From 59364c98675d5fd48e720bf44c058117620cb802 Mon Sep 17 00:00:00 2001 From: Matthew Harris Date: Thu, 24 Apr 2025 10:36:31 +0100 Subject: [PATCH 10/12] Add tests for layers --- tests/layers_test.py | 60 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 tests/layers_test.py diff --git a/tests/layers_test.py b/tests/layers_test.py new file mode 100644 index 0000000..36795f7 --- /dev/null +++ b/tests/layers_test.py @@ -0,0 +1,60 @@ +import os +import sys +sys.path.append(".") + +from ecoscape_layers import ( + LayerGenerator, + warp, + RedList, + generate_resistance_table, + in_habs, + default_refinement_method, +) + + + +species_list = ["cowpig1", "ibgshr1"] +# species_list = ["acowoo", "stejay"] + +# Paths +BASE_DIR = "." +DATA_PATH = os.path.join(BASE_DIR, "tests") + +# Load keys +REDLIST_KEY = open(os.path.join(BASE_DIR, "private/iucn_key.txt"), "r").read().strip(r' ') +EBIRD_KEY = open(os.path.join(BASE_DIR, "private/ebird_key.txt"), "r").read().strip(r' ') + +# Initialze redlist object with keys +redlist = RedList(REDLIST_KEY, EBIRD_KEY) + + +landcover_fn = os.path.join(DATA_PATH, "inputs", "test_terrain_spain.tif") + +layer_generator = LayerGenerator(landcover_fn, REDLIST_KEY, EBIRD_KEY) +redlist = RedList(REDLIST_KEY, EBIRD_KEY) + + +habitat_data = {} + +for species_code in species_list: + habitat_fn = os.path.join(DATA_PATH, "outputs", species_code, "habitat_test.tif") + resistance_dict_fn = os.path.join(DATA_PATH, "outputs", species_code, "resistance_test.csv") + range_fn = os.path.join(DATA_PATH, "outputs", species_code, "range_map_2022.gpkg") + + range_src = "ebird" + + # get IUCN Redlist Habitat data + habitat_data = redlist.get_habitat_data(species_code, ebird_code=True) + + # create the resistance csv + generate_resistance_table(habitat_data, resistance_dict_fn) + + # create the habitat layer + layer_generator.generate_habitat( + species_code, + habitat_data, + habitat_fn, + range_fn, + range_src, + current_hab_overrides = ["suitable"] + ) From c1e75f7541d03013f6cdf6039faab63e612195b7 Mon Sep 17 00:00:00 2001 From: Matthew Harris Date: Thu, 24 Apr 2025 10:41:14 +0100 Subject: [PATCH 11/12] Remove region option --- ecoscape_layers/redlist.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ecoscape_layers/redlist.py b/ecoscape_layers/redlist.py index 67d8cc5..ca892b8 100644 --- a/ecoscape_layers/redlist.py +++ b/ecoscape_layers/redlist.py @@ -70,7 +70,7 @@ def get_scientific_name(self, species_code: str) -> str: return sci_name def get_habitat_data( - self, species_name: str, region=None, ebird_code: bool = False + self, species_name: str, ebird_code: bool = False ) -> dict[int, dict[str, str | bool]]: """Gets habitat assessments for suitability for a given species. This also adds the associated landcover/terrain map's code to the API response, @@ -79,7 +79,6 @@ def get_habitat_data( Args: species_name (str): scientific name of the species. - region (_type_, optional): a specific region to assess habitats in (see https://apiv3.iucnredlist.org/api/v3/docs#regions).. Defaults to None. ebird_code (bool, optional): If True, reads species_name as an eBird species_code and converts it to a scientific/iucn name. Defaults to False. Raises: @@ -100,8 +99,6 @@ def get_habitat_data( url = f"https://api.iucnredlist.org/api/v4/taxa/scientific_name?genus_name={genus}&species_name={species}" - # if region is not None: - # url += f"/region/{region}" assessments = self.get_from_redlist(url)["assessments"] # Get assessment code for latest global assessment for species From 0f5147c992a4b8cb31f32d9060685b9b9c39ac66 Mon Sep 17 00:00:00 2001 From: Matthew Harris Date: Thu, 24 Apr 2025 10:41:43 +0100 Subject: [PATCH 12/12] ebird_code default to True --- ecoscape_layers/redlist.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ecoscape_layers/redlist.py b/ecoscape_layers/redlist.py index ca892b8..cbbf6cd 100644 --- a/ecoscape_layers/redlist.py +++ b/ecoscape_layers/redlist.py @@ -70,7 +70,7 @@ def get_scientific_name(self, species_code: str) -> str: return sci_name def get_habitat_data( - self, species_name: str, ebird_code: bool = False + self, species_name: str, ebird_code: bool = True ) -> dict[int, dict[str, str | bool]]: """Gets habitat assessments for suitability for a given species. This also adds the associated landcover/terrain map's code to the API response, @@ -79,7 +79,7 @@ def get_habitat_data( Args: species_name (str): scientific name of the species. - ebird_code (bool, optional): If True, reads species_name as an eBird species_code and converts it to a scientific/iucn name. Defaults to False. + ebird_code (bool, optional): If True, reads species_name as an eBird species_code and converts it to a scientific/iucn name. Defaults to True. Raises: ValueError: Errors when the code received from the IUCN Redlist is missing a period or data after a period.