From 5913faaf3fd9b56383e18d2aadddbdc06a7f1c7b Mon Sep 17 00:00:00 2001 From: Tyler Erickson Date: Thu, 24 Jul 2025 14:46:13 -0700 Subject: [PATCH 01/12] Add notebook for colab --- .../sustainable_sourcing_layers/colab.ipynb | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 notebooks/sustainable_sourcing_layers/colab.ipynb diff --git a/notebooks/sustainable_sourcing_layers/colab.ipynb b/notebooks/sustainable_sourcing_layers/colab.ipynb new file mode 100644 index 0000000..5623a7e --- /dev/null +++ b/notebooks/sustainable_sourcing_layers/colab.ipynb @@ -0,0 +1,72 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "KoxarHascAIB" + }, + "source": [ + "# Checkout the cloud function source code\n", + "\n", + "Cloud functions are defined from a collection of files in a [source directory](https://cloud.google.com/run/docs/write-functions#directory_structure). This section downloads cloud function definition files to Colab's storage using git's [sparse-checkout](https://git-scm.com/docs/git-sparse-checkout)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "n8nuSnu2U_hQ" + }, + "outputs": [], + "source": [ + "%%bash\n", + "is_git_repo() {\n", + " git rev-parse --is-inside-work-tree >/dev/null 2>&1\n", + "}\n", + "\n", + "# Usage:\n", + "if is_git_repo; then\n", + " echo \"This is already a git repository\"\n", + "else\n", + " echo \"This is not a git repository. Initializing git repo ---------------------\"\n", + " repo_url=https://github.com/tylere/forest-data-partnership.git\n", + " branch=sustainable-sourcing-layers-cloud-function\n", + " cloud_function_source_dir=notebooks\n", + "\n", + " # Use git sparse-checkout\n", + " git config --global init.defaultBranch $branch\n", + " git init\n", + " git remote add origin $repo_url\n", + " git sparse-checkout init --cone\n", + " git sparse-checkout set $cloud_function_source_dir\n", + " git pull origin $branch\n", + "fi" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "dUTBm0ETVj3u" + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "default", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.11.13" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} From 795b07b7ed633e8b31244dfef1e47d3a67583349 Mon Sep 17 00:00:00 2001 From: Tyler Erickson Date: Thu, 24 Jul 2025 14:54:54 -0700 Subject: [PATCH 02/12] Put cloud function config in a folder --- .../sustainable_sourcing_layers/main.py | 67 ++++++++++++++ .../requirements.txt | 6 ++ .../suso_layers_2025a.py | 87 +++++++++++++++++++ ...able_sourcing_layers_cloud_function.ipynb} | 5 +- 4 files changed, 162 insertions(+), 3 deletions(-) create mode 100644 cloud_functions/sustainable_sourcing_layers/main.py create mode 100644 cloud_functions/sustainable_sourcing_layers/requirements.txt create mode 100644 cloud_functions/sustainable_sourcing_layers/suso_layers_2025a.py rename notebooks/{sustainable_sourcing_layers/colab.ipynb => sustainable_sourcing_layers_cloud_function.ipynb} (93%) diff --git a/cloud_functions/sustainable_sourcing_layers/main.py b/cloud_functions/sustainable_sourcing_layers/main.py new file mode 100644 index 0000000..0eade68 --- /dev/null +++ b/cloud_functions/sustainable_sourcing_layers/main.py @@ -0,0 +1,67 @@ +import json +import ee +from flask import jsonify +import functions_framework +import logging +import os +import requests + +import google.auth +import google.cloud.logging +from google.api_core import retry + +from suso_layers_2025a import get_areas_image + +client = google.cloud.logging.Client() +client.setup_logging() + +@retry.Retry() +def get_suso_stats(geojson): + """Get area stats for the provided geojson polygon.""" + region = ee.Geometry(geojson) + feature_area = ee.Number(region.area(10)) + suso_image = get_areas_image() + # Sum of pixel areas in square meters. + stats = suso_image.reduceRegion( + reducer=ee.Reducer.sum(), + geometry=region, + scale=10 + ) + # Gini index. + # See https://en.wikipedia.org/wiki/Decision_tree_learning#Gini_impurity. + crop_names = ['forest', 'cocoa', 'coffee', 'palm', 'rubber'] + gini = ee.Number(1).subtract(ee.List( + [ee.Number(stats.get(c)).divide(feature_area) for c in crop_names] + ).reduce(ee.Reducer.sum())) + # Update the EE dictionary. + stats = stats.set('gini', gini).set('total_area', feature_area) + # Request the result to the client and return it. + return stats.getInfo() + + +@functions_framework.http +def main(request): + """Handle requests in a format (geojson) suitable for BigQuery.""" + credentials, _ = google.auth.default( + scopes=['https://www.googleapis.com/auth/earthengine'] + ) + ee.Initialize(credentials, project=os.environ['PROJECT']) + try: + replies = [] + request_json = request.get_json(silent=True) + calls = request_json['calls'] + for call in calls: + geo_json = json.loads(call[0]) + try: + logging.info([geo_json]) + response = get_suso_stats(geo_json) + logging.info(response) + replies.append(json.dumps(response)) + except Exception as e: + logging.error(str(e)) + replies.append(json.dumps( { "errorMessage": str(e) } )) + return jsonify(replies=replies, status=200, mimetype='application/json') + except Exception as e: + error_string = str(e) + logging.error(error_string) + return jsonify(error=error_string, status=400, mimetype='application/json') diff --git a/cloud_functions/sustainable_sourcing_layers/requirements.txt b/cloud_functions/sustainable_sourcing_layers/requirements.txt new file mode 100644 index 0000000..648703e --- /dev/null +++ b/cloud_functions/sustainable_sourcing_layers/requirements.txt @@ -0,0 +1,6 @@ +earthengine-api +flask +functions-framework +google-api-core +google-cloud-logging +requests diff --git a/cloud_functions/sustainable_sourcing_layers/suso_layers_2025a.py b/cloud_functions/sustainable_sourcing_layers/suso_layers_2025a.py new file mode 100644 index 0000000..e231d39 --- /dev/null +++ b/cloud_functions/sustainable_sourcing_layers/suso_layers_2025a.py @@ -0,0 +1,87 @@ +import google.auth +import ee +import os + +# First, initialize. +credentials, _ = google.auth.default( + scopes=['https://www.googleapis.com/auth/earthengine'] +) +ee.Initialize( + credentials, + project=os.environ['PROJECT'], + opt_url='https://earthengine-highvolume.googleapis.com' +) + +# See https://github.com/google/forest-data-partnership/tree/main/models. +cocoa_2025a = ee.ImageCollection('projects/forestdatapartnership/assets/cocoa/model_2025a') +coffee_2025a = ee.ImageCollection('projects/forestdatapartnership/assets/coffee/model_2025a') +palm_2025a = ee.ImageCollection('projects/forestdatapartnership/assets/palm/model_2025a') +rubber_2025a = ee.ImageCollection('projects/forestdatapartnership/assets/rubber/model_2025a') + +filter2020 = ee.Filter.calendarRange(2020, 2020, 'year') +filter2023 = ee.Filter.calendarRange(2023, 2023, 'year') + +cocoa2020 = cocoa_2025a.filter(filter2020).mosaic().rename('cocoa_2020') +cocoa2023 = cocoa_2025a.filter(filter2023).mosaic().rename('cocoa_2023') +coffee2020 = coffee_2025a.filter(filter2020).mosaic().rename('coffee_2020') +coffee2023 = coffee_2025a.filter(filter2023).mosaic().rename('coffee_2023') +palm2020 = palm_2025a.filter(filter2020).mosaic().rename('palm_2020') +palm2023 = palm_2025a.filter(filter2023).mosaic().rename('palm_2023') +rubber2020 = rubber_2025a.filter(filter2020).mosaic().rename('rubber_2020') +rubber2023 = rubber_2025a.filter(filter2023).mosaic().rename('rubber_2023') + +# See https://eartharxiv.org/repository/view/9085/. +natural_forest2020 = ee.ImageCollection( + 'projects/computing-engine-190414/assets/biosphere_models/public/forest_typology/natural_forest_2020_v1_0' + ).mosaic().divide(255).selfMask() + +# THRESHOLDS FOR DEMONSTRATION ONLY! Tune these to your needs. +thresholds = { + 'forest': 0.5, + 'cocoa': 0.45, + 'coffee': 0.96, + 'palm': 0.89, + 'rubber': 0.5 +} + +# A mini-ensemble of GDM and fodapa data products. +ensemble = ee.Image.cat( + natural_forest2020.rename('forest'), + cocoa2020.rename('cocoa'), + coffee2020.rename('coffee'), + palm2020.rename('palm'), + rubber2020.rename('rubber') +).unmask(0) + +# Threshold the probabilities. THRESHOLDS FOR DEMONSTRATION ONLY! +crop_names = list(thresholds.keys()) +threshold_values = list(thresholds.values()) +thresholded = ensemble.select(crop_names).gt(ee.Image(threshold_values)) + +# Unclassified means no predicted presence at the specified thresholds. +unclassified = thresholded.reduce('sum').eq(0).rename('unclassified') + +# Confusion means two or more classes predicted presence. +confusion = thresholded.reduce('sum').gt(1).selfMask().rename('confusion') + +def get_suso_layers_2025a() -> ee.Image: + '''Returns the stack of probability images in separate bands.''' + return ee.Image.cat( + natural_forest2020.rename('natural_forest_2020'), + cocoa2020.rename('cocoa_probability_2020'), + cocoa2023.rename('cocoa_probability_2023'), + coffee2020.rename('coffee_probability_2020'), + coffee2023.rename('coffee_probability_2023'), + palm2020.rename('palm_probability_2020'), + palm2023.rename('palm_probability_2023'), + rubber2020.rename('rubber_probability_2020'), + rubber2023.rename('rubber_probability_2023'), + ) + +def get_areas_image() -> ee.Image: + '''Returns data for area calculations in square meters.''' + return ee.Image.cat( + thresholded, + unclassified.rename('unclassified'), + confusion.rename('confusion') + ).multiply(ee.Image.pixelArea()) \ No newline at end of file diff --git a/notebooks/sustainable_sourcing_layers/colab.ipynb b/notebooks/sustainable_sourcing_layers_cloud_function.ipynb similarity index 93% rename from notebooks/sustainable_sourcing_layers/colab.ipynb rename to notebooks/sustainable_sourcing_layers_cloud_function.ipynb index 5623a7e..96f3d21 100644 --- a/notebooks/sustainable_sourcing_layers/colab.ipynb +++ b/notebooks/sustainable_sourcing_layers_cloud_function.ipynb @@ -24,14 +24,13 @@ " git rev-parse --is-inside-work-tree >/dev/null 2>&1\n", "}\n", "\n", - "# Usage:\n", "if is_git_repo; then\n", " echo \"This is already a git repository\"\n", "else\n", " echo \"This is not a git repository. Initializing git repo ---------------------\"\n", " repo_url=https://github.com/tylere/forest-data-partnership.git\n", - " branch=sustainable-sourcing-layers-cloud-function\n", - " cloud_function_source_dir=notebooks\n", + " branch=cloud-function-folder\n", + " cloud_function_source_dir=cloud_functions/sustainable_sourcing_layers\n", "\n", " # Use git sparse-checkout\n", " git config --global init.defaultBranch $branch\n", From 5433dac6d1219fd42f33b9931c707d92a0356166 Mon Sep 17 00:00:00 2001 From: Tyler Erickson Date: Thu, 24 Jul 2025 15:00:05 -0700 Subject: [PATCH 03/12] Update bash echo comments --- .../sustainable_sourcing_layers_cloud_function.ipynb | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/notebooks/sustainable_sourcing_layers_cloud_function.ipynb b/notebooks/sustainable_sourcing_layers_cloud_function.ipynb index 96f3d21..2210dbd 100644 --- a/notebooks/sustainable_sourcing_layers_cloud_function.ipynb +++ b/notebooks/sustainable_sourcing_layers_cloud_function.ipynb @@ -25,9 +25,9 @@ "}\n", "\n", "if is_git_repo; then\n", - " echo \"This is already a git repository\"\n", + " echo \"This is already a git repository. No action was taken.\n", "else\n", - " echo \"This is not a git repository. Initializing git repo ---------------------\"\n", + " echo \"This is not a git repository. Initiating sparse checkout...\"\n", " repo_url=https://github.com/tylere/forest-data-partnership.git\n", " branch=cloud-function-folder\n", " cloud_function_source_dir=cloud_functions/sustainable_sourcing_layers\n", @@ -62,7 +62,15 @@ "name": "python3" }, "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", "version": "3.11.13" } }, From 309180ec35d8dd6c96c501f2de633e3eecd03324 Mon Sep 17 00:00:00 2001 From: Tyler Erickson Date: Thu, 24 Jul 2025 15:01:39 -0700 Subject: [PATCH 04/12] Added missing double quote char --- notebooks/sustainable_sourcing_layers_cloud_function.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notebooks/sustainable_sourcing_layers_cloud_function.ipynb b/notebooks/sustainable_sourcing_layers_cloud_function.ipynb index 2210dbd..a36f3a8 100644 --- a/notebooks/sustainable_sourcing_layers_cloud_function.ipynb +++ b/notebooks/sustainable_sourcing_layers_cloud_function.ipynb @@ -25,7 +25,7 @@ "}\n", "\n", "if is_git_repo; then\n", - " echo \"This is already a git repository. No action was taken.\n", + " echo \"This is already a git repository. No action was taken.\"\n", "else\n", " echo \"This is not a git repository. Initiating sparse checkout...\"\n", " repo_url=https://github.com/tylere/forest-data-partnership.git\n", From 7a46407192aaf022106c98e3867fba12430a5a30 Mon Sep 17 00:00:00 2001 From: Tyler Erickson Date: Thu, 24 Jul 2025 16:22:16 -0700 Subject: [PATCH 05/12] Move get_suso_stats to the helpers file --- .../sustainable_sourcing_layers/main.py | 27 ---------------- .../suso_layers_2025a.py | 31 +++++++++++++++++-- 2 files changed, 28 insertions(+), 30 deletions(-) diff --git a/cloud_functions/sustainable_sourcing_layers/main.py b/cloud_functions/sustainable_sourcing_layers/main.py index 0eade68..f84c025 100644 --- a/cloud_functions/sustainable_sourcing_layers/main.py +++ b/cloud_functions/sustainable_sourcing_layers/main.py @@ -8,37 +8,10 @@ import google.auth import google.cloud.logging -from google.api_core import retry - -from suso_layers_2025a import get_areas_image client = google.cloud.logging.Client() client.setup_logging() -@retry.Retry() -def get_suso_stats(geojson): - """Get area stats for the provided geojson polygon.""" - region = ee.Geometry(geojson) - feature_area = ee.Number(region.area(10)) - suso_image = get_areas_image() - # Sum of pixel areas in square meters. - stats = suso_image.reduceRegion( - reducer=ee.Reducer.sum(), - geometry=region, - scale=10 - ) - # Gini index. - # See https://en.wikipedia.org/wiki/Decision_tree_learning#Gini_impurity. - crop_names = ['forest', 'cocoa', 'coffee', 'palm', 'rubber'] - gini = ee.Number(1).subtract(ee.List( - [ee.Number(stats.get(c)).divide(feature_area) for c in crop_names] - ).reduce(ee.Reducer.sum())) - # Update the EE dictionary. - stats = stats.set('gini', gini).set('total_area', feature_area) - # Request the result to the client and return it. - return stats.getInfo() - - @functions_framework.http def main(request): """Handle requests in a format (geojson) suitable for BigQuery.""" diff --git a/cloud_functions/sustainable_sourcing_layers/suso_layers_2025a.py b/cloud_functions/sustainable_sourcing_layers/suso_layers_2025a.py index e231d39..698cf23 100644 --- a/cloud_functions/sustainable_sourcing_layers/suso_layers_2025a.py +++ b/cloud_functions/sustainable_sourcing_layers/suso_layers_2025a.py @@ -1,7 +1,9 @@ -import google.auth -import ee import os +import ee +import google.auth +from google.api_core import retry + # First, initialize. credentials, _ = google.auth.default( scopes=['https://www.googleapis.com/auth/earthengine'] @@ -84,4 +86,27 @@ def get_areas_image() -> ee.Image: thresholded, unclassified.rename('unclassified'), confusion.rename('confusion') - ).multiply(ee.Image.pixelArea()) \ No newline at end of file + ).multiply(ee.Image.pixelArea()) + +@retry.Retry() +def get_suso_stats(geojson): + """Get area stats for the provided geojson polygon.""" + region = ee.Geometry(geojson) + feature_area = ee.Number(region.area(10)) + suso_image = get_areas_image() + # Sum of pixel areas in square meters. + stats = suso_image.reduceRegion( + reducer=ee.Reducer.sum(), + geometry=region, + scale=10 + ) + # Gini index. + # See https://en.wikipedia.org/wiki/Decision_tree_learning#Gini_impurity. + crop_names = ['forest', 'cocoa', 'coffee', 'palm', 'rubber'] + gini = ee.Number(1).subtract(ee.List( + [ee.Number(stats.get(c)).divide(feature_area) for c in crop_names] + ).reduce(ee.Reducer.sum())) + # Update the EE dictionary. + stats = stats.set('gini', gini).set('total_area', feature_area) + # Request the result to the client and return it. + return stats.getInfo() \ No newline at end of file From 29645f3dcbc0699302e819bf720e3e8f7dfac71d Mon Sep 17 00:00:00 2001 From: Tyler Erickson Date: Thu, 24 Jul 2025 16:37:18 -0700 Subject: [PATCH 06/12] add function import for get_suso_stats --- cloud_functions/sustainable_sourcing_layers/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cloud_functions/sustainable_sourcing_layers/main.py b/cloud_functions/sustainable_sourcing_layers/main.py index f84c025..918d0f5 100644 --- a/cloud_functions/sustainable_sourcing_layers/main.py +++ b/cloud_functions/sustainable_sourcing_layers/main.py @@ -9,6 +9,8 @@ import google.auth import google.cloud.logging +from suso_layers_2025a import get_suso_stats + client = google.cloud.logging.Client() client.setup_logging() From 333a5012367c6accdb14b8d3085d6d5b49349bb9 Mon Sep 17 00:00:00 2001 From: Tyler Erickson Date: Thu, 24 Jul 2025 16:41:41 -0700 Subject: [PATCH 07/12] deploy the cloud run function and test it --- ...nable_sourcing_layers_cloud_function.ipynb | 482 +++++++++++++++++- 1 file changed, 479 insertions(+), 3 deletions(-) diff --git a/notebooks/sustainable_sourcing_layers_cloud_function.ipynb b/notebooks/sustainable_sourcing_layers_cloud_function.ipynb index a36f3a8..8b04bf4 100644 --- a/notebooks/sustainable_sourcing_layers_cloud_function.ipynb +++ b/notebooks/sustainable_sourcing_layers_cloud_function.ipynb @@ -1,5 +1,22 @@ { "cells": [ + { + "cell_type": "markdown", + "source": [ + "# Sustainable Sourcing Layers in a Cloud Function\n", + "\n", + "This notebook demonstrates deployment of Google Cloud functions to get commodity and forest information in a format designed to be integrated to other workflows.\n", + "\n", + "The 2025a release of Forest Data Partnership sustainable sourcing layers including palm, rubber, cocoa and coffee are used for commodity information. For details on how these layers were produced, see [the technical documentation on GitHub](https://github.com/google/forest-data-partnership/tree/main/models). In particular, see [the limitations](https://github.com/google/forest-data-partnership/tree/main/models#limitations). See also the [Forest Data Partnership publisher catalog](https://developers.google.com/earth-engine/datasets/publisher/forestdatapartnership) for dataset descriptions. See [this Earth Engine Code Editor script](https://goo.gle/fodapa-layers) for a demonstration of how choice of thresholds affects the mapped results.\n", + "\n", + "Note that users of commercial projects will need to request access to the Forest Data Partnership datasets with [this form](https://docs.google.com/forms/d/e/1FAIpQLSe7L3eh6t2JIPqEtAQwXwY7ZmW52v8W5vrIi4QN_XYgTNJZLw/viewform).\n", + "\n", + "**WARNING**: These demos consume billable resources and may result in charges to your account!" + ], + "metadata": { + "id": "RDYfoFn6e0G3" + } + }, { "cell_type": "markdown", "metadata": { @@ -42,6 +59,17 @@ "fi" ] }, + { + "cell_type": "markdown", + "source": [ + "# Setup and Auth\n", + "\n", + "Import the Python libraries used by this notebook." + ], + "metadata": { + "id": "vbXOEZtIe7tb" + } + }, { "cell_type": "code", "execution_count": null, @@ -49,12 +77,460 @@ "id": "dUTBm0ETVj3u" }, "outputs": [], - "source": [] + "source": [ + "import importlib\n", + "import json\n", + "import os\n", + "from pprint import pprint\n", + "import sys\n", + "\n", + "import ee\n", + "from google.colab import userdata\n", + "\n", + "print('Using EE version: ', ee.__version__)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "This notebook assumes that the GCP project and compute region information are stored as Colab secrets. This avoids storing user-specific and potentially sensitive information in the notebook. See this guide for information on how to use Colab secrets." + ], + "metadata": { + "id": "iYkUyHrQfFLF" + } + }, + { + "cell_type": "code", + "source": [ + "# Load from Colab secrets\n", + "PROJECT = userdata.get('EE_PROJECT_ID')\n", + "REGION = userdata.get('GCP_REGION')\n", + "\n", + "# Store as environment variables\n", + "os.environ['PROJECT'] = PROJECT\n", + "os.environ['REGION'] = REGION" + ], + "metadata": { + "id": "isACEYGufCQL" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "Authenticate using the Google Cloud CLI (gcloud)." + ], + "metadata": { + "id": "gadkSfi5fLrJ" + } + }, + { + "cell_type": "code", + "source": [ + "!gcloud auth login --project {PROJECT} --billing-project {PROJECT} --update-adc" + ], + "metadata": { + "id": "M6Y6XiiJfIPq" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "# Load WHISP example data\n", + "\n", + "We will test out the Cloud Run function using some example data from\n", + "[WHISP](xhttps://openforis.org/solutions/whisp/).\n", + "\n", + "Start by downloading some WHISP example data from GitHub and convert it to an `ee.FeatureCollection`." + ], + "metadata": { + "id": "8VPkk3LBkGhf" + } + }, + { + "cell_type": "code", + "source": [ + "fc_list = !curl https://raw.githubusercontent.com/forestdatapartnership/whisp/main/tests/fixtures/geojson_example.geojson\n", + "fc_obj = json.loads(\"\\n\".join(fc_list))\n", + "features = fc_obj['features']" + ], + "metadata": { + "id": "bPd9lD_bkQkW" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "Print an example feature." + ], + "metadata": { + "id": "d4cKpH8RkcPr" + } + }, + { + "cell_type": "code", + "source": [ + "features[0]" + ], + "metadata": { + "id": "dlRGneG0kdCM" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "Extract the feature geometries." + ], + "metadata": { + "id": "vz10es_pkwNJ" + } + }, + { + "cell_type": "code", + "source": [ + "geoms = [f['geometry'] for f in features]" + ], + "metadata": { + "id": "_1lziQOukw6C" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "Print an example geometry.\n" + ], + "metadata": { + "id": "pfKL-XBYkzY1" + } + }, + { + "cell_type": "code", + "source": [ + "geoms[0]" + ], + "metadata": { + "id": "1KTGwy3uk5UT" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "# Test the code\n", + "\n", + "Import the code and run a few tests." + ], + "metadata": { + "id": "R4cAROMhhCyX" + } + }, + { + "cell_type": "code", + "source": [ + "from cloud_functions.sustainable_sourcing_layers import suso_layers_2025a\n", + "\n", + "# Check the image metadata and bands.\n", + "layers = suso_layers_2025a.get_suso_layers_2025a()\n", + "pprint(layers.getInfo())" + ], + "metadata": { + "id": "rquPJFwzlf1x" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "Sample the layers at a specified point." + ], + "metadata": { + "id": "qApFqpfHmfO4" + } + }, + { + "cell_type": "code", + "source": [ + "test_point = ee.Geometry.Point(104.33, -3.41)\n", + "pprint(layers.reduceRegion(ee.Reducer.mean(), test_point, 10).getInfo())" + ], + "metadata": { + "id": "FBk5a_V8hI8l" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "Get the class areas at a specific point" + ], + "metadata": { + "id": "xjsDiE8xnCzw" + } + }, + { + "cell_type": "code", + "source": [ + "print(\n", + " suso_layers_2025a\n", + " .get_areas_image()\n", + " .reduceRegion(\n", + " ee.Reducer.mean(),\n", + " test_point,\n", + " 10\n", + " ).getInfo()\n", + ")" + ], + "metadata": { + "id": "-2wJQYcYlGd3" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "Get statistics within an example geometry." + ], + "metadata": { + "id": "r1SnGr7_nR5s" + } + }, + { + "cell_type": "code", + "source": [ + "suso_layers_2025a.get_suso_stats(geojson=geoms[0])" + ], + "metadata": { + "id": "5a89qkMeiMqQ" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "# Deploy Cloud Run function\n", + "\n", + "See [this quickstart](https://cloud.google.com/run/docs/quickstarts/functions/deploy-functions-gcloud) for details on deploying Cloud Run functions.\n", + "\n", + "The Cloud Run function configuration files are stored in the folder `cloud_functions/sustainable_sourcing_layers`." + ], + "metadata": { + "id": "f8DwusJvnk0f" + } + }, + { + "cell_type": "markdown", + "source": [ + "Inspect the list of Python packages required to run the Cloud Run function." + ], + "metadata": { + "id": "2VLBq9bwh1bQ" + } + }, + { + "cell_type": "code", + "source": [ + "%cat cloud_functions/sustainable_sourcing_layers/requirements.txt" + ], + "metadata": { + "id": "6gJ5JXfbfOKQ" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "## Deploy the Cloud Run function\n", + "\n", + "Use the following `gcloud` command to deploy the Cloud Run function.\n", + "\n", + "*Note that the Compute Engine default service account is being used for authentication to Earth Engine. For commercial access to the sustainable sourcing layers, ensure that the service account is approved for commercial access ([request form](https://docs.google.com/forms/d/e/1FAIpQLSe7L3eh6t2JIPqEtAQwXwY7ZmW52v8W5vrIi4QN_XYgTNJZLw/viewform)).*" + ], + "metadata": { + "id": "Lur5Fe7SoyIy" + } + }, + { + "cell_type": "code", + "source": [ + "!gcloud functions deploy 'suso_function' \\\n", + " --gen2 \\\n", + " --region={REGION} \\\n", + " --project={PROJECT} \\\n", + " --runtime=python312 \\\n", + " --source='cloud_functions/sustainable_sourcing_layers' \\\n", + " --set-env-vars PROJECT={PROJECT} \\\n", + " --entry-point=main \\\n", + " --trigger-http \\\n", + " --no-allow-unauthenticated \\\n", + " --timeout=300s" + ], + "metadata": { + "id": "9G96vr2io_gQ" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + " It's helpful to follow the link in the deployment output to monitor and/or debug your Cloud Run function. In particular, see the metrics and logs tabs to help resolve perfpormance issues." + ], + "metadata": { + "id": "8YaUZnYqo8OK" + } + }, + { + "cell_type": "markdown", + "source": [ + "## Test the deployed Cloud Function\n", + "\n" + ], + "metadata": { + "id": "ORIfMTYTp-XC" + } + }, + { + "cell_type": "markdown", + "source": [ + "Make a test request out of the WHISP sample data geometries." + ], + "metadata": { + "id": "FQdOsf3Kqn3I" + } + }, + { + "cell_type": "code", + "source": [ + "# test_calls = [[json.dumps(g), 'foo_string', 'bar_string'] for g in geoms]\n", + "test_calls = [[json.dumps(g), 'foo_string'] for g in geoms]\n", + "test_calls[:3]" + ], + "metadata": { + "id": "KMRvo-8Bgsc7" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "Create a formatted request." + ], + "metadata": { + "id": "_5hPKl6jq1VQ" + } + }, + { + "cell_type": "code", + "source": [ + "test_request = json.dumps({'calls': test_calls}, separators=(',', ':')).join(\"''\")\n", + "test_request" + ], + "metadata": { + "id": "i01h0RTwqqXF" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "Use curl to submit the test request. This might take a while." + ], + "metadata": { + "id": "Qo1cRl_wrBpg" + } + }, + { + "cell_type": "code", + "source": [ + "responses = !curl -X POST https://{REGION}-{PROJECT}.cloudfunctions.net/suso_function \\\n", + " -H \"Authorization: bearer $(gcloud auth print-identity-token)\" \\\n", + " -H \"Content-Type: application/json\" \\\n", + " -d {test_request}" + ], + "metadata": { + "id": "VTZqNgfRq2jV" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "Inspect the output of the function." + ], + "metadata": { + "id": "bTeBYf12rJkI" + } + }, + { + "cell_type": "code", + "source": [ + "response = responses[0]\n", + "response_json = json.loads(response)\n", + "replies_json = [json.loads(reply) for reply in response_json['replies']]\n", + "reply_keys = replies_json[0].keys()\n", + "\n", + "print(f'{len(responses) = }')\n", + "print(f'{len(replies_json) = }')\n", + "print(f'{reply_keys = }')" + ], + "metadata": { + "id": "rYv1u1qrrEI2" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "Pretty print the replies." + ], + "metadata": { + "id": "FrE4tZ-ZraDJ" + } + }, + { + "cell_type": "code", + "source": [ + "print(json.dumps(replies_json, indent=4))" + ], + "metadata": { + "id": "WrlloZKBrOu5" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "source": [], + "metadata": { + "id": "VuRp08v1uuxP" + }, + "execution_count": null, + "outputs": [] } ], "metadata": { "colab": { - "provenance": [] + "provenance": [], + "toc_visible": true }, "kernelspec": { "display_name": "default", @@ -76,4 +552,4 @@ }, "nbformat": 4, "nbformat_minor": 0 -} +} \ No newline at end of file From cab690b95ab77387e050418c391cfc8ec1b815d3 Mon Sep 17 00:00:00 2001 From: Tyler Erickson Date: Fri, 25 Jul 2025 11:30:05 -0700 Subject: [PATCH 08/12] Adds section demoing how to call Cloud Function using Python --- ...nable_sourcing_layers_cloud_function.ipynb | 292 +++++++++++++----- 1 file changed, 221 insertions(+), 71 deletions(-) diff --git a/notebooks/sustainable_sourcing_layers_cloud_function.ipynb b/notebooks/sustainable_sourcing_layers_cloud_function.ipynb index 8b04bf4..45a9360 100644 --- a/notebooks/sustainable_sourcing_layers_cloud_function.ipynb +++ b/notebooks/sustainable_sourcing_layers_cloud_function.ipynb @@ -19,52 +19,19 @@ }, { "cell_type": "markdown", - "metadata": { - "id": "KoxarHascAIB" - }, "source": [ - "# Checkout the cloud function source code\n", - "\n", - "Cloud functions are defined from a collection of files in a [source directory](https://cloud.google.com/run/docs/write-functions#directory_structure). This section downloads cloud function definition files to Colab's storage using git's [sparse-checkout](https://git-scm.com/docs/git-sparse-checkout)." - ] - }, - { - "cell_type": "code", - "execution_count": null, + "# Setup" + ], "metadata": { - "id": "n8nuSnu2U_hQ" - }, - "outputs": [], - "source": [ - "%%bash\n", - "is_git_repo() {\n", - " git rev-parse --is-inside-work-tree >/dev/null 2>&1\n", - "}\n", - "\n", - "if is_git_repo; then\n", - " echo \"This is already a git repository. No action was taken.\"\n", - "else\n", - " echo \"This is not a git repository. Initiating sparse checkout...\"\n", - " repo_url=https://github.com/tylere/forest-data-partnership.git\n", - " branch=cloud-function-folder\n", - " cloud_function_source_dir=cloud_functions/sustainable_sourcing_layers\n", - "\n", - " # Use git sparse-checkout\n", - " git config --global init.defaultBranch $branch\n", - " git init\n", - " git remote add origin $repo_url\n", - " git sparse-checkout init --cone\n", - " git sparse-checkout set $cloud_function_source_dir\n", - " git pull origin $branch\n", - "fi" - ] + "id": "yaw0K_p2ieqt" + } }, { "cell_type": "markdown", "source": [ - "# Setup and Auth\n", + "## Set the Colab Environment environment variables\n", "\n", - "Import the Python libraries used by this notebook." + "Import the Python libraries used by the section." ], "metadata": { "id": "vbXOEZtIe7tb" @@ -78,16 +45,10 @@ }, "outputs": [], "source": [ - "import importlib\n", "import json\n", "import os\n", - "from pprint import pprint\n", - "import sys\n", - "\n", - "import ee\n", - "from google.colab import userdata\n", "\n", - "print('Using EE version: ', ee.__version__)" + "from google.colab import userdata" ] }, { @@ -119,6 +80,8 @@ { "cell_type": "markdown", "source": [ + "## Authenticate\n", + "\n", "Authenticate using the Google Cloud CLI (gcloud)." ], "metadata": { @@ -139,7 +102,7 @@ { "cell_type": "markdown", "source": [ - "# Load WHISP example data\n", + "## Load WHISP example data\n", "\n", "We will test out the Cloud Run function using some example data from\n", "[WHISP](xhttps://openforis.org/solutions/whisp/).\n", @@ -226,14 +189,79 @@ { "cell_type": "markdown", "source": [ - "# Test the code\n", + "# Create the Cloud Run function" + ], + "metadata": { + "id": "xewQ7PFghhuY" + } + }, + { + "cell_type": "markdown", + "metadata": { + "id": "KoxarHascAIB" + }, + "source": [ + "## Checkout the Cloud Run function source code\n", + "\n", + "Cloud Run functions are defined from a collection of files in a [source directory](https://cloud.google.com/run/docs/write-functions#directory_structure). This section downloads Cloud Run function definition files to Colab's storage using git's [sparse-checkout](https://git-scm.com/docs/git-sparse-checkout)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "n8nuSnu2U_hQ" + }, + "outputs": [], + "source": [ + "%%bash\n", + "is_git_repo() {\n", + " git rev-parse --is-inside-work-tree >/dev/null 2>&1\n", + "}\n", + "\n", + "if is_git_repo; then\n", + " echo \"This is already a git repository. No action was taken.\"\n", + "else\n", + " echo \"This is not a git repository. Initiating sparse checkout...\"\n", + " repo_url=https://github.com/tylere/forest-data-partnership.git\n", + " branch=cloud-function-folder\n", + " cloud_function_source_dir=cloud_functions/sustainable_sourcing_layers\n", + "\n", + " # Use git sparse-checkout\n", + " git config --global init.defaultBranch $branch\n", + " git init\n", + " git remote add origin $repo_url\n", + " git sparse-checkout init --cone\n", + " git sparse-checkout set $cloud_function_source_dir\n", + " git pull origin $branch\n", + "fi" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Test the Cloud Run function code\n", "\n", - "Import the code and run a few tests." + "Import the code and run a few tests locally." ], "metadata": { "id": "R4cAROMhhCyX" } }, + { + "cell_type": "code", + "source": [ + "from pprint import pprint\n", + "import ee\n", + "\n", + "print('Using EE version: ', ee.__version__)" + ], + "metadata": { + "id": "U7FynjCmvvKl" + }, + "execution_count": null, + "outputs": [] + }, { "cell_type": "code", "source": [ @@ -321,7 +349,7 @@ { "cell_type": "markdown", "source": [ - "# Deploy Cloud Run function\n", + "## Deploy the Cloud Run function\n", "\n", "See [this quickstart](https://cloud.google.com/run/docs/quickstarts/functions/deploy-functions-gcloud) for details on deploying Cloud Run functions.\n", "\n", @@ -354,7 +382,7 @@ { "cell_type": "markdown", "source": [ - "## Deploy the Cloud Run function\n", + "## Use gcloud to Deploy the Cloud Run function\n", "\n", "Use the following `gcloud` command to deploy the Cloud Run function.\n", "\n", @@ -397,8 +425,11 @@ { "cell_type": "markdown", "source": [ - "## Test the deployed Cloud Function\n", - "\n" + "# Call the deployed Cloud Run function\n", + "\n", + "This section demonstrates how to call the deployed Cloud Run function using different tools.\n", + "\n", + "*(Note that after creating a new Colab runtime you must first run the [Setup](#scrollTo=yaw0K_p2ieqt) section before running this section.)*" ], "metadata": { "id": "ORIfMTYTp-XC" @@ -407,21 +438,34 @@ { "cell_type": "markdown", "source": [ - "Make a test request out of the WHISP sample data geometries." + "## Call using curl\n", + "\n", + "The section demonstrates calling Cloud Run function using [curl](https://curl.se/), a general purpose command-line tool used to transfer data to or from a server using a wide range of network protocols." + ], + "metadata": { + "id": "ZQVTTrJrgDhs" + } + }, + { + "cell_type": "markdown", + "source": [ + "Create a JSON formatted string of the Cloud Run function inputs (i.e. WHISP sample geometries)." ], "metadata": { - "id": "FQdOsf3Kqn3I" + "id": "_5hPKl6jq1VQ" } }, { "cell_type": "code", "source": [ - "# test_calls = [[json.dumps(g), 'foo_string', 'bar_string'] for g in geoms]\n", - "test_calls = [[json.dumps(g), 'foo_string'] for g in geoms]\n", - "test_calls[:3]" + "payload = {\n", + " \"calls\": [[json.dumps(g)] for g in geoms]\n", + "}\n", + "payload_json = json.dumps(payload, separators=(',', ':'))\n", + "payload_json" ], "metadata": { - "id": "KMRvo-8Bgsc7" + "id": "i01h0RTwqqXF" }, "execution_count": null, "outputs": [] @@ -429,20 +473,20 @@ { "cell_type": "markdown", "source": [ - "Create a formatted request." + "Format the payload for curl, by wrapping it in escaped single quotes." ], "metadata": { - "id": "_5hPKl6jq1VQ" + "id": "87sKKIDkqtv7" } }, { "cell_type": "code", "source": [ - "test_request = json.dumps({'calls': test_calls}, separators=(',', ':')).join(\"''\")\n", - "test_request" + "post_data = f\"'{payload_json}'\"\n", + "post_data" ], "metadata": { - "id": "i01h0RTwqqXF" + "id": "4jxcpFeTsfHx" }, "execution_count": null, "outputs": [] @@ -450,7 +494,7 @@ { "cell_type": "markdown", "source": [ - "Use curl to submit the test request. This might take a while." + "Use curl to submit a test POST data. This might take a few seconds to execute." ], "metadata": { "id": "Qo1cRl_wrBpg" @@ -459,10 +503,11 @@ { "cell_type": "code", "source": [ - "responses = !curl -X POST https://{REGION}-{PROJECT}.cloudfunctions.net/suso_function \\\n", + "response_body_text_list = !curl -X POST https://{REGION}-{PROJECT}.cloudfunctions.net/suso_function \\\n", " -H \"Authorization: bearer $(gcloud auth print-identity-token)\" \\\n", " -H \"Content-Type: application/json\" \\\n", - " -d {test_request}" + " -d {post_data}\n", + "response_body_text_list" ], "metadata": { "id": "VTZqNgfRq2jV" @@ -473,7 +518,7 @@ { "cell_type": "markdown", "source": [ - "Inspect the output of the function." + "Use Python to parse and inspect the Cloud Run function response." ], "metadata": { "id": "bTeBYf12rJkI" @@ -482,12 +527,11 @@ { "cell_type": "code", "source": [ - "response = responses[0]\n", + "response = response_body_text_list[0]\n", "response_json = json.loads(response)\n", "replies_json = [json.loads(reply) for reply in response_json['replies']]\n", "reply_keys = replies_json[0].keys()\n", "\n", - "print(f'{len(responses) = }')\n", "print(f'{len(replies_json) = }')\n", "print(f'{reply_keys = }')" ], @@ -517,11 +561,117 @@ "execution_count": null, "outputs": [] }, + { + "cell_type": "markdown", + "source": [ + "## Call using Python" + ], + "metadata": { + "id": "nRtdxiW9Uz3K" + } + }, + { + "cell_type": "code", + "source": [ + "import google.auth.transport.requests\n", + "import requests" + ], + "metadata": { + "id": "t305EBW_dF8f" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "Retrieve your Google Cloud account identity token." + ], + "metadata": { + "id": "c1matw9LVHJU" + } + }, + { + "cell_type": "code", + "source": [ + "# Set this to the target audience (Cloud Run service URL)\n", + "audience = \"https://us-west1-vorgeo-training.cloudfunctions.net/suso_function\"\n", + "\n", + "project = os.environ['PROJECT']\n", + "credentials, _ = google.auth.default(\n", + " scopes=['https://www.googleapis.com/auth/earthengine'],\n", + ")\n", + "credentials.refresh(\n", + " request = google.auth.transport.requests.Request()\n", + ")\n", + "identity_token = credentials.id_token\n", + "\n", + "print(f'{identity_token = }')" + ], + "metadata": { + "id": "LcqZ5dDqWR1N" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "Make a call to the Cloud Run function." + ], + "metadata": { + "id": "oijzeN0OdH6g" + } + }, + { + "cell_type": "code", + "source": [ + "headers = {\n", + " \"Authorization\": f\"Bearer {identity_token}\",\n", + " \"Content-Type\": \"application/json\"\n", + "}\n", + "\n", + "# Prepare the payload in the format expected by the Cloud Function\n", + "payload = {\n", + " \"calls\": [[json.dumps(g)] for g in geoms]\n", + "}\n", + "\n", + "response = requests.post(audience, headers=headers, json=payload)\n", + "print(response.text)" + ], + "metadata": { + "id": "8ALAR16kc0E1" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "Parse the Cloud Run function response." + ], + "metadata": { + "id": "1vc0WRLvfeyc" + } + }, + { + "cell_type": "code", + "source": [ + "response_json = json.loads(response.text)\n", + "replies_json = [json.loads(reply) for reply in response_json['replies']]\n", + "print(json.dumps(replies_json, indent=4))" + ], + "metadata": { + "id": "k0SAf0iHde7q" + }, + "execution_count": null, + "outputs": [] + }, { "cell_type": "code", "source": [], "metadata": { - "id": "VuRp08v1uuxP" + "id": "-pYn8XANfFsr" }, "execution_count": null, "outputs": [] From 38da1d6933ee9c293f63b9d0a37270831e37da96 Mon Sep 17 00:00:00 2001 From: Tyler Erickson Date: Fri, 25 Jul 2025 11:32:56 -0700 Subject: [PATCH 09/12] Add back the Next Steps section. --- ...ustainable_sourcing_layers_cloud_function.ipynb | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/notebooks/sustainable_sourcing_layers_cloud_function.ipynb b/notebooks/sustainable_sourcing_layers_cloud_function.ipynb index 45a9360..1bda560 100644 --- a/notebooks/sustainable_sourcing_layers_cloud_function.ipynb +++ b/notebooks/sustainable_sourcing_layers_cloud_function.ipynb @@ -668,13 +668,15 @@ "outputs": [] }, { - "cell_type": "code", - "source": [], + "cell_type": "markdown", + "source": [ + "# Next Steps\n", + "\n", + "- Try the [WHISP Cloud Function demo notebook](https://colab.research.google.com/drive/1NCaPOoxqmAEWb8c8V0kEHVunbW1yHVkL?resourcekey=0-HJ3ou94AbjdKkkvaPW1Jtw&usp=sharing)." + ], "metadata": { - "id": "-pYn8XANfFsr" - }, - "execution_count": null, - "outputs": [] + "id": "7QPlRfQe22Qm" + } } ], "metadata": { From 862c5bd8590618388cb96a3b0b0cd3e81ef0b722 Mon Sep 17 00:00:00 2001 From: Tyler Erickson Date: Mon, 28 Jul 2025 14:46:50 -0500 Subject: [PATCH 10/12] Update environment variable name for GCP project --- .../sustainable_sourcing_layers/suso_layers_2025a.py | 2 +- notebooks/sustainable_sourcing_layers_cloud_function.ipynb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud_functions/sustainable_sourcing_layers/suso_layers_2025a.py b/cloud_functions/sustainable_sourcing_layers/suso_layers_2025a.py index 698cf23..489c97c 100644 --- a/cloud_functions/sustainable_sourcing_layers/suso_layers_2025a.py +++ b/cloud_functions/sustainable_sourcing_layers/suso_layers_2025a.py @@ -10,7 +10,7 @@ ) ee.Initialize( credentials, - project=os.environ['PROJECT'], + project=os.environ['GOOGLE_CLOUD_PROJECT'], opt_url='https://earthengine-highvolume.googleapis.com' ) diff --git a/notebooks/sustainable_sourcing_layers_cloud_function.ipynb b/notebooks/sustainable_sourcing_layers_cloud_function.ipynb index 1bda560..d04bdcd 100644 --- a/notebooks/sustainable_sourcing_layers_cloud_function.ipynb +++ b/notebooks/sustainable_sourcing_layers_cloud_function.ipynb @@ -68,7 +68,7 @@ "REGION = userdata.get('GCP_REGION')\n", "\n", "# Store as environment variables\n", - "os.environ['PROJECT'] = PROJECT\n", + "os.environ['GOOGLE_CLOUD_PROJECT'] = PROJECT\n", "os.environ['REGION'] = REGION" ], "metadata": { From 8f6181cfb6340fcd8da6b438aa5cdf28b5a186d6 Mon Sep 17 00:00:00 2001 From: Tyler Erickson Date: Thu, 7 Aug 2025 16:50:34 -0700 Subject: [PATCH 11/12] Update project env var name --- cloud_functions/sustainable_sourcing_layers/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud_functions/sustainable_sourcing_layers/main.py b/cloud_functions/sustainable_sourcing_layers/main.py index 918d0f5..70155ba 100644 --- a/cloud_functions/sustainable_sourcing_layers/main.py +++ b/cloud_functions/sustainable_sourcing_layers/main.py @@ -20,7 +20,7 @@ def main(request): credentials, _ = google.auth.default( scopes=['https://www.googleapis.com/auth/earthengine'] ) - ee.Initialize(credentials, project=os.environ['PROJECT']) + ee.Initialize(credentials, project=os.environ['GOOGLE_CLOUD_PROJECT']) try: replies = [] request_json = request.get_json(silent=True) From 1966d3cdc9fdc6b39cc2953838faed1a5f635c02 Mon Sep 17 00:00:00 2001 From: Tyler Erickson Date: Fri, 8 Aug 2025 16:27:25 -0700 Subject: [PATCH 12/12] Update env var names and add map display. --- ...nable_sourcing_layers_cloud_function.ipynb | 79 +++++++++++++++++-- 1 file changed, 73 insertions(+), 6 deletions(-) diff --git a/notebooks/sustainable_sourcing_layers_cloud_function.ipynb b/notebooks/sustainable_sourcing_layers_cloud_function.ipynb index d04bdcd..1c79934 100644 --- a/notebooks/sustainable_sourcing_layers_cloud_function.ipynb +++ b/notebooks/sustainable_sourcing_layers_cloud_function.ipynb @@ -1,5 +1,15 @@ { "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "view-in-github", + "colab_type": "text" + }, + "source": [ + "\"Open" + ] + }, { "cell_type": "markdown", "source": [ @@ -64,12 +74,12 @@ "cell_type": "code", "source": [ "# Load from Colab secrets\n", - "PROJECT = userdata.get('EE_PROJECT_ID')\n", - "REGION = userdata.get('GCP_REGION')\n", + "PROJECT = userdata.get('GOOGLE_CLOUD_PROJECT')\n", + "REGION = userdata.get('GOOGLE_CLOUD_LOCATION')\n", "\n", "# Store as environment variables\n", "os.environ['GOOGLE_CLOUD_PROJECT'] = PROJECT\n", - "os.environ['REGION'] = REGION" + "os.environ['GOOGLE_CLOUD_LOCATION'] = REGION" ], "metadata": { "id": "isACEYGufCQL" @@ -401,7 +411,7 @@ " --project={PROJECT} \\\n", " --runtime=python312 \\\n", " --source='cloud_functions/sustainable_sourcing_layers' \\\n", - " --set-env-vars PROJECT={PROJECT} \\\n", + " --set-env-vars GOOGLE_CLOUD_PROJECT={PROJECT} \\\n", " --entry-point=main \\\n", " --trigger-http \\\n", " --no-allow-unauthenticated \\\n", @@ -597,7 +607,7 @@ "# Set this to the target audience (Cloud Run service URL)\n", "audience = \"https://us-west1-vorgeo-training.cloudfunctions.net/suso_function\"\n", "\n", - "project = os.environ['PROJECT']\n", + "project = os.environ['GOOGLE_CLOUD_PROJECT']\n", "credentials, _ = google.auth.default(\n", " scopes=['https://www.googleapis.com/auth/earthengine'],\n", ")\n", @@ -667,6 +677,62 @@ "execution_count": null, "outputs": [] }, + { + "cell_type": "markdown", + "source": [ + "### Display the features" + ], + "metadata": { + "id": "RhCObMj6zWdo" + } + }, + { + "cell_type": "markdown", + "source": [ + "For each feature, update the properties to include the Cloud Run function results." + ], + "metadata": { + "id": "LLlcdooTqW0y" + } + }, + { + "cell_type": "code", + "source": [ + "for i in range(len(features)):\n", + " features[i]['properties'].update(replies_json[i])" + ], + "metadata": { + "id": "ftKrM7Sgs7A-" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "Display the updated features on a map (styled red). Click on the features to inspect their properties." + ], + "metadata": { + "id": "RkUaGjpLy2Ps" + } + }, + { + "cell_type": "code", + "source": [ + "import geemap\n", + "\n", + "Map = geemap.Map(center=(1.9332, 6.3830), zoom=5)\n", + "Map.add_basemap('SATELLITE')\n", + "Map.add_inspector()\n", + "Map.addLayer(ee.FeatureCollection(features), {'color': 'FF0000'}, 'Features')\n", + "Map" + ], + "metadata": { + "id": "8-yRpjIwte9h" + }, + "execution_count": null, + "outputs": [] + }, { "cell_type": "markdown", "source": [ @@ -682,7 +748,8 @@ "metadata": { "colab": { "provenance": [], - "toc_visible": true + "toc_visible": true, + "include_colab_link": true }, "kernelspec": { "display_name": "default",